gladguy commited on
Commit
8d22bb4
Β·
1 Parent(s): 88dcc5f

Initial deployment

Browse files
Files changed (7) hide show
  1. README.md +0 -14
  2. app.py +320 -0
  3. packages.txt +0 -0
  4. requirements.txt +0 -0
  5. static/index.html +108 -0
  6. static/script.js +304 -0
  7. static/style.css +305 -0
README.md CHANGED
@@ -1,14 +0,0 @@
1
- ---
2
- title: SimpleViva
3
- emoji: πŸ‘
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 6.0.1
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- short_description: Anatomy Viva verse
12
- ----
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse, JSONResponse
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ import json
6
+ import time
7
+ import base64
8
+ import io
9
+ import os
10
+ from typing import Dict, Optional
11
+ import torch
12
+ import numpy as np
13
+ from scipy.io.wavfile import write
14
+
15
+ # Initialize FastAPI app
16
+ app = FastAPI(title="Anatomy Viva Voice App", version="1.0.0")
17
+
18
+ # Mount static files
19
+ app.mount("/static", StaticFiles(directory="static"), name="static")
20
+
21
+ # CORS middleware
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ class FreeVoiceTTS:
31
+ def __init__(self):
32
+ self.model = None
33
+ self.device = "cpu"
34
+
35
+ def load_silero_tts(self):
36
+ """Load Silero TTS - lightweight and reliable"""
37
+ try:
38
+ import torch
39
+
40
+ device = torch.device('cpu')
41
+ torch.set_num_threads(4)
42
+
43
+ model, example_text = torch.hub.load(
44
+ repo_or_dir='snakers4/silero-models',
45
+ model='silero_tts',
46
+ language='en',
47
+ speaker='v3_en'
48
+ )
49
+ self.silero_model = model
50
+ return True
51
+ except Exception as e:
52
+ print(f"Silero TTS loading failed: {e}")
53
+ return False
54
+
55
+ def text_to_speech_silero(self, text: str) -> bytes:
56
+ """Convert text to speech using Silero TTS"""
57
+ try:
58
+ if not hasattr(self, 'silero_model'):
59
+ if not self.load_silero_tts():
60
+ raise Exception("Silero TTS not available")
61
+
62
+ # Generate audio using Silero
63
+ audio = self.silero_model.apply_tts(
64
+ text=text,
65
+ speaker='en_0', # English female voice
66
+ sample_rate=24000
67
+ )
68
+
69
+ # Convert to bytes
70
+ audio_buffer = io.BytesIO()
71
+ write(audio_buffer, 24000, (audio * 32767).numpy().astype(np.int16))
72
+
73
+ return audio_buffer.getvalue()
74
+
75
+ except Exception as e:
76
+ print(f"Silero TTS failed: {e}")
77
+ return self.generate_silence()
78
+
79
+ def generate_silence(self) -> bytes:
80
+ """Generate 1 second of silence as fallback"""
81
+ silence = np.zeros(24000, dtype=np.int16)
82
+ audio_buffer = io.BytesIO()
83
+ write(audio_buffer, 24000, silence)
84
+ return audio_buffer.getvalue()
85
+
86
+ # Initialize TTS
87
+ tts_engine = FreeVoiceTTS()
88
+
89
+ class AnatomyProfessor:
90
+ def __init__(self):
91
+ self.current_topic = None
92
+ self.conversation_history = []
93
+ self.question_bank = self._initialize_question_bank()
94
+
95
+ def _initialize_question_bank(self):
96
+ return {
97
+ "upper_limb": [
98
+ {
99
+ "question": "Describe the course and distribution of the median nerve from its origin to the hand.",
100
+ "key_points": ["brachial plexus roots C5-T1", "medial and lateral cords", "carpal tunnel", "LOAF muscles"],
101
+ "follow_up": "What clinical condition results from median nerve compression at the wrist?",
102
+ "difficulty": "medium"
103
+ },
104
+ {
105
+ "question": "Explain the brachial plexus in detail, including its major branches.",
106
+ "key_points": ["roots, trunks, divisions, cords, branches", "mnemonic: Real Texans Drink Cold Beer", "musculocutaneous, axillary, radial, median, ulnar nerves"],
107
+ "follow_up": "Which cord of the brachial plexus is most vulnerable in shoulder dislocations?",
108
+ "difficulty": "hard"
109
+ },
110
+ {
111
+ "question": "What are the muscles of the rotator cuff and their functions?",
112
+ "key_points": ["supraspinatus", "infraspinatus", "teres minor", "subscapularis", "SITS mnemonic"],
113
+ "follow_up": "Which rotator cuff muscle is most commonly injured?",
114
+ "difficulty": "medium"
115
+ }
116
+ ],
117
+ "lower_limb": [
118
+ {
119
+ "question": "Trace the course of the sciatic nerve from its origin to its terminal branches.",
120
+ "key_points": ["L4-S3 roots", "passes through greater sciatic foramen", "divides into tibial and common fibular nerves", "innervates hamstrings"],
121
+ "follow_up": "What are the clinical manifestations of sciatic nerve injury?",
122
+ "difficulty": "medium"
123
+ },
124
+ {
125
+ "question": "Describe the boundaries and contents of the femoral triangle.",
126
+ "key_points": ["inguinal ligament", "sartorius", "adductor longus", "femoral nerve, artery, vein", "NAVY arrangement"],
127
+ "follow_up": "Why is the femoral triangle important clinically?",
128
+ "difficulty": "medium"
129
+ }
130
+ ],
131
+ "cardiology": [
132
+ {
133
+ "question": "Describe the blood supply to the heart and the coronary circulation.",
134
+ "key_points": ["left and right coronary arteries", "circumflex artery", "left anterior descending", "coronary sinus"],
135
+ "follow_up": "Which coronary artery is most commonly involved in myocardial infarction?",
136
+ "difficulty": "medium"
137
+ },
138
+ {
139
+ "question": "Explain the conduction system of the heart.",
140
+ "key_points": ["SA node", "AV node", "bundle of His", "bundle branches", "Purkinje fibers"],
141
+ "follow_up": "What is the clinical significance of the AV node?",
142
+ "difficulty": "hard"
143
+ }
144
+ ],
145
+ "neuroanatomy": [
146
+ {
147
+ "question": "Describe the blood supply of the brain.",
148
+ "key_points": ["internal carotid arteries", "vertebral arteries", "circle of Willis", "anterior, middle, posterior cerebral arteries"],
149
+ "follow_up": "What is the clinical consequence of middle cerebral artery occlusion?",
150
+ "difficulty": "hard"
151
+ },
152
+ {
153
+ "question": "Name the twelve cranial nerves and their basic functions.",
154
+ "key_points": ["olfactory, optic, oculomotor, trochlear, trigeminal, abducens, facial, vestibulocochlear, glossopharyngeal, vagus, accessory, hypoglossal"],
155
+ "follow_up": "Which cranial nerve has the longest intracranial course?",
156
+ "difficulty": "medium"
157
+ }
158
+ ]
159
+ }
160
+
161
+ def set_topic(self, topic: str):
162
+ self.current_topic = topic
163
+ self.conversation_history = []
164
+
165
+ def get_next_question(self) -> Dict:
166
+ """Get the next question for the current topic"""
167
+ if not self.current_topic or self.current_topic not in self.question_bank:
168
+ return {"error": "Invalid topic selected"}
169
+
170
+ asked_indices = [conv.get("question_index", -1) for conv in self.conversation_history]
171
+
172
+ for i, question_data in enumerate(self.question_bank[self.current_topic]):
173
+ if i not in asked_indices:
174
+ return {
175
+ "question": question_data["question"],
176
+ "question_index": i,
177
+ "key_points": question_data["key_points"],
178
+ "difficulty": question_data["difficulty"]
179
+ }
180
+
181
+ return {"question": "You have completed all questions for this topic. Excellent work!", "completed": True}
182
+
183
+ def evaluate_answer(self, question_index: int, student_answer: str) -> Dict:
184
+ """Evaluate student's answer and provide feedback"""
185
+ if self.current_topic not in self.question_bank or question_index >= len(self.question_bank[self.current_topic]):
186
+ return {"error": "Invalid question index"}
187
+
188
+ question_data = self.question_bank[self.current_topic][question_index]
189
+
190
+ # Enhanced evaluation
191
+ evaluation = self._evaluate_answer_comprehensive(question_data, student_answer)
192
+
193
+ self.conversation_history.append({
194
+ "question_index": question_index,
195
+ "question": question_data["question"],
196
+ "answer": student_answer,
197
+ "feedback": evaluation["feedback"],
198
+ "score": evaluation["score"],
199
+ "timestamp": time.time()
200
+ })
201
+
202
+ return {
203
+ "feedback": evaluation["feedback"],
204
+ "score": evaluation["score"],
205
+ "next_question": self.get_next_question(),
206
+ "conversation_history": self.conversation_history
207
+ }
208
+
209
+ def _evaluate_answer_comprehensive(self, question_data: Dict, answer: str) -> Dict:
210
+ """Comprehensive answer evaluation"""
211
+
212
+ base_score = self._calculate_comprehensiveness(answer, question_data["key_points"])
213
+
214
+ # Generate appropriate feedback
215
+ if base_score >= 8:
216
+ feedback = f"Excellent! You demonstrated thorough understanding. {question_data.get('follow_up', '')}"
217
+ elif base_score >= 6:
218
+ feedback = f"Good attempt. You covered main concepts well. {question_data.get('follow_up', 'Consider the clinical applications.')}"
219
+ else:
220
+ missed_points = self._get_missed_points(answer, question_data["key_points"])
221
+ feedback = f"Let me help you improve. Key aspects: {', '.join(missed_points)}. {question_data.get('follow_up', '')}"
222
+
223
+ return {
224
+ "feedback": feedback,
225
+ "score": base_score
226
+ }
227
+
228
+ def _calculate_comprehensiveness(self, answer: str, key_points: list) -> float:
229
+ """Calculate score based on coverage of key points"""
230
+ answer_lower = answer.lower()
231
+ covered_points = sum(1 for point in key_points if any(word in answer_lower for word in point.lower().split()))
232
+ return min(10, (covered_points / len(key_points)) * 10)
233
+
234
+ def _get_missed_points(self, answer: str, key_points: list) -> list:
235
+ """Get points that were missed in the answer"""
236
+ answer_lower = answer.lower()
237
+ missed = []
238
+ for point in key_points:
239
+ if not any(word in answer_lower for word in point.lower().split()):
240
+ missed.append(point)
241
+ return missed if missed else key_points[:2] # Return first two if all covered
242
+
243
+ # Global professor instance
244
+ professor = AnatomyProfessor()
245
+
246
+ # Serve the main page
247
+ @app.get("/")
248
+ async def read_index():
249
+ return FileResponse('static/index.html')
250
+
251
+ # API Routes
252
+ @app.post("/api/start_session")
253
+ async def start_session(topic: str):
254
+ """Start a new viva session"""
255
+ professor.set_topic(topic)
256
+ first_question = professor.get_next_question()
257
+
258
+ return JSONResponse({
259
+ "status": "started",
260
+ "topic": topic,
261
+ "first_question": first_question,
262
+ "message": f"Viva session started on {topic}"
263
+ })
264
+
265
+ @app.post("/api/text_to_speech")
266
+ async def text_to_speech(text: str):
267
+ """Convert text to speech using free TTS"""
268
+ try:
269
+ audio_data = tts_engine.text_to_speech_silero(text)
270
+
271
+ return JSONResponse({
272
+ "audio_data": base64.b64encode(audio_data).decode('utf-8'),
273
+ "text": text,
274
+ "format": "wav"
275
+ })
276
+
277
+ except Exception as e:
278
+ raise HTTPException(status_code=500, detail=f"TTS failed: {str(e)}")
279
+
280
+ @app.post("/api/evaluate_answer")
281
+ async def evaluate_answer(question_index: int, answer: str):
282
+ """Evaluate student's answer"""
283
+ try:
284
+ evaluation = professor.evaluate_answer(question_index, answer)
285
+ return JSONResponse(evaluation)
286
+ except Exception as e:
287
+ raise HTTPException(status_code=500, detail=f"Evaluation failed: {str(e)}")
288
+
289
+ @app.get("/api/topics")
290
+ async def get_topics():
291
+ """Get available anatomy topics"""
292
+ return JSONResponse({
293
+ "topics": {
294
+ "upper_limb": "Upper Limb Anatomy",
295
+ "lower_limb": "Lower Limb Anatomy",
296
+ "cardiology": "Cardiac Anatomy",
297
+ "neuroanatomy": "Neuroanatomy"
298
+ }
299
+ })
300
+
301
+ @app.get("/api/health")
302
+ async def health_check():
303
+ """Health check endpoint"""
304
+ return JSONResponse({"status": "healthy", "timestamp": time.time()})
305
+
306
+ # Initialize TTS on startup
307
+ @app.on_event("startup")
308
+ async def startup_event():
309
+ """Initialize TTS models on app startup"""
310
+ print("Initializing Anatomy Viva App...")
311
+ success = tts_engine.load_silero_tts()
312
+ if success:
313
+ print("Silero TTS initialized successfully")
314
+ else:
315
+ print("TTS initialization failed - will use fallbacks")
316
+
317
+ # For Hugging Face Spaces - they look for this
318
+ if __name__ == "__main__":
319
+ import uvicorn
320
+ uvicorn.run(app, host="0.0.0.0", port=7860)
packages.txt ADDED
File without changes
requirements.txt ADDED
File without changes
static/index.html ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Anatomy Viva Voce - Hugging Face</title>
8
+ <link rel="stylesheet" href="/static/style.css">
9
+ </head>
10
+
11
+ <body>
12
+ <div class="container">
13
+ <header>
14
+ <h1>🧠 Anatomy Viva Voce Simulator</h1>
15
+ <p>Practice medical anatomy with AI Professor - Deployed on Hugging Face</p>
16
+ </header>
17
+
18
+ <div class="topic-selection" id="topicSelection">
19
+ <h2>Select Anatomy Topic</h2>
20
+ <div class="topic-grid">
21
+ <div class="topic-card" onclick="startSession('upper_limb')">
22
+ <h3>Upper Limb</h3>
23
+ <p>Brachial plexus, nerves, muscles</p>
24
+ </div>
25
+ <div class="topic-card" onclick="startSession('lower_limb')">
26
+ <h3>Lower Limb</h3>
27
+ <p>Sciatic nerve, femoral triangle</p>
28
+ </div>
29
+ <div class="topic-card" onclick="startSession('cardiology')">
30
+ <h3>Cardiology</h3>
31
+ <p>Heart anatomy, coronary circulation</p>
32
+ </div>
33
+ <div class="topic-card" onclick="startSession('neuroanatomy')">
34
+ <h3>Neuroanatomy</h3>
35
+ <p>Brain, cranial nerves, blood supply</p>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="viva-session hidden" id="vivaSession">
41
+ <div class="session-header">
42
+ <h2 id="sessionTopic">Topic: Upper Limb</h2>
43
+ <div class="session-controls">
44
+ <span class="score-display">Score: <span id="totalScore">0</span></span>
45
+ <button onclick="resetSession()" class="btn-secondary">End Session</button>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="conversation-area">
50
+ <div class="professor-message">
51
+ <div class="avatar">πŸ‘¨β€πŸ«</div>
52
+ <div class="message-content">
53
+ <strong>Professor:</strong>
54
+ <div id="questionText">Let's begin with your first question...</div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="voice-controls">
59
+ <button id="speakQuestionBtn" onclick="speakCurrentQuestion()" class="btn-voice">
60
+ πŸ”Š Speak Question
61
+ </button>
62
+ <div id="listeningIndicator" class="listening-indicator hidden">
63
+ <div class="pulse"></div>
64
+ <span>Professor is speaking...</span>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="answer-section">
69
+ <h3>Your Answer:</h3>
70
+ <textarea id="studentAnswer" placeholder="Type your answer here or use voice input..."></textarea>
71
+
72
+ <div class="answer-buttons">
73
+ <button onclick="submitAnswer()" class="btn-primary">Submit Answer</button>
74
+ <button id="voiceBtn" onclick="toggleVoiceInput()" class="btn-voice">
75
+ 🎀 Voice Input
76
+ </button>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="feedback-area hidden" id="feedbackArea">
81
+ <div class="professor-message">
82
+ <div class="avatar">πŸ‘¨β€πŸ«</div>
83
+ <div class="message-content">
84
+ <strong>Feedback:</strong>
85
+ <div id="feedbackText"></div>
86
+ <div class="score-badge">Score: <span id="scoreValue">0</span>/10</div>
87
+ <button onclick="nextQuestion()" class="btn-primary" id="nextButton">
88
+ Next Question
89
+ </button>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="progress-section">
96
+ <h3>Session Progress</h3>
97
+ <div id="progressBar" class="progress-bar">
98
+ <div class="progress-fill" style="width: 0%"></div>
99
+ </div>
100
+ <div id="conversationHistory" class="history-container"></div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <script src="/static/script.js"></script>
106
+ </body>
107
+
108
+ </html>
static/script.js ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let currentSession = {
2
+ topic: null,
3
+ currentQuestion: null,
4
+ questionIndex: null,
5
+ totalScore: 0,
6
+ questionCount: 0
7
+ };
8
+
9
+ // Voice recognition setup
10
+ let recognition = null;
11
+ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
12
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
13
+ recognition = new SpeechRecognition();
14
+ recognition.continuous = false;
15
+ recognition.interimResults = false;
16
+ recognition.lang = 'en-US';
17
+
18
+ recognition.onresult = (event) => {
19
+ const transcript = event.results[0][0].transcript;
20
+ document.getElementById('studentAnswer').value = transcript;
21
+ };
22
+
23
+ recognition.onerror = (event) => {
24
+ console.error('Speech recognition error:', event.error);
25
+ stopVoiceInput();
26
+ };
27
+ }
28
+
29
+ async function startSession(topic) {
30
+ try {
31
+ const response = await fetch('/api/start_session', {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ body: JSON.stringify({ topic: topic })
37
+ });
38
+
39
+ const data = await response.json();
40
+
41
+ if (data.status === 'started') {
42
+ currentSession.topic = topic;
43
+ currentSession.totalScore = 0;
44
+ currentSession.questionCount = 0;
45
+
46
+ displayQuestion(data.first_question);
47
+
48
+ document.getElementById('sessionTopic').textContent = `Topic: ${getTopicName(topic)}`;
49
+ document.getElementById('topicSelection').classList.add('hidden');
50
+ document.getElementById('vivaSession').classList.remove('hidden');
51
+
52
+ updateProgress();
53
+ }
54
+
55
+ } catch (error) {
56
+ console.error('Error starting session:', error);
57
+ alert('Failed to start session. Please try again.');
58
+ }
59
+ }
60
+
61
+ function getTopicName(topicKey) {
62
+ const topicNames = {
63
+ 'upper_limb': 'Upper Limb Anatomy',
64
+ 'lower_limb': 'Lower Limb Anatomy',
65
+ 'cardiology': 'Cardiac Anatomy',
66
+ 'neuroanatomy': 'Neuroanatomy'
67
+ };
68
+ return topicNames[topicKey] || topicKey;
69
+ }
70
+
71
+ function displayQuestion(questionData) {
72
+ if (questionData.completed) {
73
+ endSession();
74
+ return;
75
+ }
76
+
77
+ currentSession.currentQuestion = questionData.question;
78
+ currentSession.questionIndex = questionData.question_index;
79
+
80
+ document.getElementById('questionText').textContent = questionData.question;
81
+ document.getElementById('studentAnswer').value = '';
82
+ document.getElementById('feedbackArea').classList.add('hidden');
83
+
84
+ updateProgress();
85
+ }
86
+
87
+ async function speakCurrentQuestion() {
88
+ const questionText = currentSession.currentQuestion;
89
+ if (!questionText) return;
90
+
91
+ const listeningIndicator = document.getElementById('listeningIndicator');
92
+ listeningIndicator.classList.remove('hidden');
93
+
94
+ try {
95
+ const response = await fetch('/api/text_to_speech', {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ },
100
+ body: JSON.stringify({ text: questionText })
101
+ });
102
+
103
+ const data = await response.json();
104
+
105
+ if (data.audio_data) {
106
+ await playAudioData(data.audio_data);
107
+ }
108
+ } catch (error) {
109
+ console.error('TTS failed:', error);
110
+ // Fallback to browser TTS
111
+ speakWithBrowserTTS(questionText);
112
+ } finally {
113
+ listeningIndicator.classList.add('hidden');
114
+ }
115
+ }
116
+
117
+ async function playAudioData(base64Audio) {
118
+ return new Promise((resolve) => {
119
+ const audioBlob = base64ToBlob(base64Audio, 'audio/wav');
120
+ const audioUrl = URL.createObjectURL(audioBlob);
121
+ const audio = new Audio(audioUrl);
122
+
123
+ audio.onended = () => {
124
+ URL.revokeObjectURL(audioUrl);
125
+ resolve();
126
+ };
127
+
128
+ audio.onerror = () => resolve();
129
+
130
+ audio.play().catch(error => {
131
+ console.error('Audio play failed:', error);
132
+ resolve();
133
+ });
134
+ });
135
+ }
136
+
137
+ function speakWithBrowserTTS(text) {
138
+ if ('speechSynthesis' in window) {
139
+ const utterance = new SpeechSynthesisUtterance(text);
140
+ utterance.rate = 0.8;
141
+ utterance.pitch = 1.0;
142
+
143
+ const voices = speechSynthesis.getVoices();
144
+ const englishVoice = voices.find(voice => voice.lang.startsWith('en-'));
145
+ if (englishVoice) {
146
+ utterance.voice = englishVoice;
147
+ }
148
+
149
+ speechSynthesis.speak(utterance);
150
+ }
151
+ }
152
+
153
+ function toggleVoiceInput() {
154
+ const voiceBtn = document.getElementById('voiceBtn');
155
+
156
+ if (!recognition) {
157
+ alert('Speech recognition not supported in this browser. Please use Chrome or Edge.');
158
+ return;
159
+ }
160
+
161
+ if (voiceBtn.textContent.includes('Start')) {
162
+ startVoiceInput();
163
+ } else {
164
+ stopVoiceInput();
165
+ }
166
+ }
167
+
168
+ function startVoiceInput() {
169
+ const voiceBtn = document.getElementById('voiceBtn');
170
+ voiceBtn.textContent = 'πŸ›‘ Stop Listening';
171
+ voiceBtn.style.background = '#e74c3c';
172
+
173
+ recognition.start();
174
+ }
175
+
176
+ function stopVoiceInput() {
177
+ const voiceBtn = document.getElementById('voiceBtn');
178
+ voiceBtn.textContent = '🎀 Voice Input';
179
+ voiceBtn.style.background = '#9b59b6';
180
+
181
+ if (recognition) {
182
+ recognition.stop();
183
+ }
184
+ }
185
+
186
+ async function submitAnswer() {
187
+ const answer = document.getElementById('studentAnswer').value.trim();
188
+
189
+ if (!answer) {
190
+ alert('Please enter your answer');
191
+ return;
192
+ }
193
+
194
+ try {
195
+ const response = await fetch('/api/evaluate_answer', {
196
+ method: 'POST',
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ },
200
+ body: JSON.stringify({
201
+ question_index: currentSession.questionIndex,
202
+ answer: answer
203
+ })
204
+ });
205
+
206
+ const data = await response.json();
207
+
208
+ displayFeedback(data);
209
+
210
+ } catch (error) {
211
+ console.error('Error submitting answer:', error);
212
+ alert('Failed to submit answer. Please try again.');
213
+ }
214
+ }
215
+
216
+ function displayFeedback(data) {
217
+ currentSession.totalScore += data.score;
218
+ currentSession.questionCount++;
219
+
220
+ document.getElementById('feedbackText').textContent = data.feedback;
221
+ document.getElementById('scoreValue').textContent = data.score.toFixed(1);
222
+ document.getElementById('totalScore').textContent = currentSession.totalScore.toFixed(1);
223
+ document.getElementById('feedbackArea').classList.remove('hidden');
224
+
225
+ updateProgress();
226
+ addToHistory(data);
227
+ }
228
+
229
+ function addToHistory(data) {
230
+ const historyContainer = document.getElementById('conversationHistory');
231
+ const historyItem = document.createElement('div');
232
+ historyItem.className = 'history-item';
233
+
234
+ historyItem.innerHTML = `
235
+ <strong>Q${currentSession.questionCount}:</strong> ${currentSession.currentQuestion}<br>
236
+ <strong>Your Answer:</strong> ${data.transcribed_answer || document.getElementById('studentAnswer').value.substring(0, 100)}...<br>
237
+ <strong>Score:</strong> ${data.score.toFixed(1)}/10
238
+ `;
239
+
240
+ historyContainer.appendChild(historyItem);
241
+ historyContainer.scrollTop = historyContainer.scrollHeight;
242
+ }
243
+
244
+ function updateProgress() {
245
+ const progressFill = document.querySelector('.progress-fill');
246
+ if (currentSession.questionCount > 0) {
247
+ const progress = (currentSession.questionCount / 5) * 100; // Assuming 5 questions per topic
248
+ progressFill.style.width = `${Math.min(progress, 100)}%`;
249
+ } else {
250
+ progressFill.style.width = '0%';
251
+ }
252
+ }
253
+
254
+ function nextQuestion() {
255
+ // In a real implementation, this would get the next question from the server
256
+ // For now, we'll simulate by ending the session after a few questions
257
+ if (currentSession.questionCount >= 3) {
258
+ endSession();
259
+ } else {
260
+ // Simulate getting next question
261
+ const nextQuestion = {
262
+ question: `Next question about ${currentSession.topic}...`,
263
+ question_index: currentSession.questionIndex + 1
264
+ };
265
+ displayQuestion(nextQuestion);
266
+ }
267
+ }
268
+
269
+ function endSession() {
270
+ const averageScore = currentSession.totalScore / currentSession.questionCount;
271
+ alert(`Session completed! Your average score: ${averageScore.toFixed(1)}/10\nWell done!`);
272
+ resetSession();
273
+ }
274
+
275
+ function resetSession() {
276
+ currentSession = {
277
+ topic: null,
278
+ currentQuestion: null,
279
+ questionIndex: null,
280
+ totalScore: 0,
281
+ questionCount: 0
282
+ };
283
+
284
+ document.getElementById('vivaSession').classList.add('hidden');
285
+ document.getElementById('topicSelection').classList.remove('hidden');
286
+ document.getElementById('conversationHistory').innerHTML = '';
287
+ document.getElementById('totalScore').textContent = '0';
288
+ }
289
+
290
+ // Utility function
291
+ function base64ToBlob(base64, mimeType) {
292
+ const byteCharacters = atob(base64);
293
+ const byteNumbers = new Array(byteCharacters.length);
294
+ for (let i = 0; i < byteCharacters.length; i++) {
295
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
296
+ }
297
+ const byteArray = new Uint8Array(byteNumbers);
298
+ return new Blob([byteArray], { type: mimeType });
299
+ }
300
+
301
+ // Initialize speech synthesis voices
302
+ if ('speechSynthesis' in window) {
303
+ speechSynthesis.getVoices();
304
+ }
static/style.css ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ padding: 20px;
12
+ line-height: 1.6;
13
+ }
14
+
15
+ .container {
16
+ max-width: 1200px;
17
+ margin: 0 auto;
18
+ background: white;
19
+ border-radius: 15px;
20
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
21
+ overflow: hidden;
22
+ }
23
+
24
+ header {
25
+ background: #2c3e50;
26
+ color: white;
27
+ padding: 30px;
28
+ text-align: center;
29
+ }
30
+
31
+ header h1 {
32
+ margin-bottom: 10px;
33
+ font-size: 2.5em;
34
+ }
35
+
36
+ header p {
37
+ opacity: 0.9;
38
+ font-size: 1.1em;
39
+ }
40
+
41
+ .topic-selection,
42
+ .viva-session {
43
+ padding: 40px;
44
+ }
45
+
46
+ .topic-grid {
47
+ display: grid;
48
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
49
+ gap: 20px;
50
+ margin-top: 30px;
51
+ }
52
+
53
+ .topic-card {
54
+ background: #f8f9fa;
55
+ padding: 25px;
56
+ border-radius: 10px;
57
+ text-align: center;
58
+ cursor: pointer;
59
+ transition: all 0.3s ease;
60
+ border: 2px solid #e9ecef;
61
+ }
62
+
63
+ .topic-card:hover {
64
+ transform: translateY(-5px);
65
+ border-color: #3498db;
66
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
67
+ }
68
+
69
+ .topic-card h3 {
70
+ color: #2c3e50;
71
+ margin-bottom: 10px;
72
+ }
73
+
74
+ .topic-card p {
75
+ color: #7f8c8d;
76
+ font-size: 0.9em;
77
+ }
78
+
79
+ .hidden {
80
+ display: none;
81
+ }
82
+
83
+ .session-header {
84
+ display: flex;
85
+ justify-content: space-between;
86
+ align-items: center;
87
+ margin-bottom: 30px;
88
+ padding-bottom: 20px;
89
+ border-bottom: 2px solid #ecf0f1;
90
+ }
91
+
92
+ .session-controls {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 15px;
96
+ }
97
+
98
+ .score-display {
99
+ background: #27ae60;
100
+ color: white;
101
+ padding: 8px 15px;
102
+ border-radius: 20px;
103
+ font-weight: bold;
104
+ }
105
+
106
+ .btn-primary,
107
+ .btn-secondary,
108
+ .btn-voice {
109
+ padding: 12px 25px;
110
+ border: none;
111
+ border-radius: 6px;
112
+ cursor: pointer;
113
+ font-size: 1em;
114
+ transition: all 0.3s ease;
115
+ }
116
+
117
+ .btn-primary {
118
+ background: #3498db;
119
+ color: white;
120
+ }
121
+
122
+ .btn-primary:hover {
123
+ background: #2980b9;
124
+ }
125
+
126
+ .btn-secondary {
127
+ background: #e74c3c;
128
+ color: white;
129
+ }
130
+
131
+ .btn-secondary:hover {
132
+ background: #c0392b;
133
+ }
134
+
135
+ .btn-voice {
136
+ background: #9b59b6;
137
+ color: white;
138
+ }
139
+
140
+ .btn-voice:hover {
141
+ background: #8e44ad;
142
+ }
143
+
144
+ .professor-message {
145
+ display: flex;
146
+ gap: 15px;
147
+ margin-bottom: 20px;
148
+ align-items: flex-start;
149
+ }
150
+
151
+ .avatar {
152
+ font-size: 2em;
153
+ flex-shrink: 0;
154
+ }
155
+
156
+ .message-content {
157
+ flex: 1;
158
+ background: #f8f9fa;
159
+ padding: 20px;
160
+ border-radius: 10px;
161
+ border-left: 4px solid #3498db;
162
+ }
163
+
164
+ .voice-controls {
165
+ margin: 20px 0;
166
+ text-align: center;
167
+ }
168
+
169
+ .listening-indicator {
170
+ display: inline-flex;
171
+ align-items: center;
172
+ gap: 10px;
173
+ background: #fff3cd;
174
+ padding: 10px 20px;
175
+ border-radius: 25px;
176
+ border: 1px solid #ffeaa7;
177
+ }
178
+
179
+ .pulse {
180
+ width: 12px;
181
+ height: 12px;
182
+ background: #e74c3c;
183
+ border-radius: 50%;
184
+ animation: pulse 1.5s infinite;
185
+ }
186
+
187
+ @keyframes pulse {
188
+ 0% {
189
+ transform: scale(0.95);
190
+ opacity: 0.7;
191
+ }
192
+
193
+ 50% {
194
+ transform: scale(1.1);
195
+ opacity: 1;
196
+ }
197
+
198
+ 100% {
199
+ transform: scale(0.95);
200
+ opacity: 0.7;
201
+ }
202
+ }
203
+
204
+ .answer-section {
205
+ margin: 30px 0;
206
+ }
207
+
208
+ .answer-section h3 {
209
+ margin-bottom: 15px;
210
+ color: #2c3e50;
211
+ }
212
+
213
+ #studentAnswer {
214
+ width: 100%;
215
+ height: 120px;
216
+ padding: 15px;
217
+ border: 2px solid #bdc3c7;
218
+ border-radius: 8px;
219
+ font-size: 1em;
220
+ resize: vertical;
221
+ font-family: inherit;
222
+ }
223
+
224
+ .answer-buttons {
225
+ display: flex;
226
+ gap: 15px;
227
+ margin-top: 15px;
228
+ flex-wrap: wrap;
229
+ }
230
+
231
+ .feedback-area {
232
+ margin-top: 30px;
233
+ }
234
+
235
+ .score-badge {
236
+ display: inline-block;
237
+ background: #27ae60;
238
+ color: white;
239
+ padding: 8px 15px;
240
+ border-radius: 15px;
241
+ margin: 10px 0;
242
+ font-weight: bold;
243
+ }
244
+
245
+ .progress-section {
246
+ margin-top: 40px;
247
+ padding-top: 20px;
248
+ border-top: 2px solid #ecf0f1;
249
+ }
250
+
251
+ .progress-bar {
252
+ width: 100%;
253
+ height: 10px;
254
+ background: #ecf0f1;
255
+ border-radius: 5px;
256
+ overflow: hidden;
257
+ margin: 15px 0;
258
+ }
259
+
260
+ .progress-fill {
261
+ height: 100%;
262
+ background: #27ae60;
263
+ transition: width 0.3s ease;
264
+ }
265
+
266
+ .history-container {
267
+ max-height: 300px;
268
+ overflow-y: auto;
269
+ margin-top: 20px;
270
+ }
271
+
272
+ .history-item {
273
+ background: #f8f9fa;
274
+ padding: 15px;
275
+ margin: 10px 0;
276
+ border-radius: 8px;
277
+ border-left: 4px solid #95a5a6;
278
+ }
279
+
280
+ /* Responsive design */
281
+ @media (max-width: 768px) {
282
+ .container {
283
+ margin: 10px;
284
+ border-radius: 10px;
285
+ }
286
+
287
+ .topic-selection,
288
+ .viva-session {
289
+ padding: 20px;
290
+ }
291
+
292
+ .session-header {
293
+ flex-direction: column;
294
+ gap: 15px;
295
+ align-items: flex-start;
296
+ }
297
+
298
+ .answer-buttons {
299
+ flex-direction: column;
300
+ }
301
+
302
+ header h1 {
303
+ font-size: 2em;
304
+ }
305
+ }