Tas01 commited on
Commit
dcae6be
Β·
verified Β·
1 Parent(s): dbccd7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -231
app.py CHANGED
@@ -4,26 +4,29 @@ import tempfile
4
  import os
5
  import base64
6
  from gtts import gTTS
7
- import speech_recognition as sr
8
- import torch
9
- from transformers import CLIPProcessor, CLIPModel
10
- from PIL import Image
11
  import io
12
  import numpy as np
13
  import json
14
  from datetime import datetime
 
15
 
16
  # ============================================
17
- # 1. LOAD PDF (NO UPLOAD NEEDED)
18
  # ============================================
19
 
20
- PDF_PATH = "2VBMAPP.pdf" # Your pre-loaded PDF file
21
 
22
- class ABATutor: # FIXED: No space in class name
23
  def __init__(self):
24
- # Load CLIP for computer vision
25
- self.clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
26
- self.clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
 
 
 
 
27
 
28
  # Load PDF
29
  try:
@@ -31,227 +34,234 @@ class ABATutor: # FIXED: No space in class name
31
  self.total_pages = len(self.doc)
32
  print(f"βœ… Loaded PDF with {self.total_pages} pages")
33
  except Exception as e:
34
- print(f"⚠️ Could not load PDF: {e}")
35
  self.doc = None
36
  self.total_pages = 1
37
 
38
- self.current_page = 6 # Start at first Tact page (page 7 in PDF, index 6)
39
-
40
- # Session data
41
  self.session_data = []
42
- self.current_trial = {}
 
 
43
 
44
- # Expected labels for each page (built from book content)
45
- self.page_labels = self.build_label_map()
46
-
47
- print(f"βœ… CLIP model ready")
48
-
49
- def build_label_map(self):
50
- """Map page numbers to expected object labels based on VB-MAPP book"""
51
- # These are the target objects from your book pages
52
- return {
53
  6: ["cat", "dog", "ball", "shoe", "car", "apple", "bird", "fish", "hat", "cup"],
54
  7: ["cat", "dog", "ball", "shoe", "car", "apple"],
55
  8: ["bird", "fish", "hat", "cup", "book", "chair"],
56
  9: ["train", "plane", "boat", "truck", "bus", "bike"],
57
  10: ["frog", "monkey", "elephant", "lion", "giraffe", "zebra"],
58
- # Add more pages as needed
59
  }
60
 
61
- def get_current_image(self):
62
- """Extract current page as PIL Image for CV"""
63
  if self.doc is None:
64
- # Return a blank image if PDF not loaded
65
  return Image.new('RGB', (800, 600), color='white')
66
 
67
  page = self.doc.load_page(self.current_page)
68
- pix = page.get_pixmap(dpi=100)
69
  img_data = pix.tobytes("png")
70
  pil_img = Image.open(io.BytesIO(img_data))
71
- return pil_img
72
-
73
- def get_current_image_html(self):
74
- """Get HTML image for display"""
75
- if self.doc is None:
76
- return '<p style="text-align:center;padding:50px;">⚠️ PDF not loaded. Please ensure 2VBMAPP.pdf is in the Space.</p>'
77
 
78
- page = self.doc.load_page(self.current_page)
79
- pix = page.get_pixmap(dpi=100)
80
- img_data = pix.tobytes("png")
81
- b64 = base64.b64encode(img_data).decode()
82
- return f'<img src="data:image/png;base64,{b64}" style="max-width:100%; border-radius:10px; box-shadow:0 4px 6px rgba(0,0,0,0.1);" />'
83
-
84
- def cv_detect_object(self, spoken_word, image=None):
85
- """
86
- Use CLIP to check if spoken word matches objects in image
87
- Returns confidence score and whether correct
88
- """
89
- if image is None:
90
- image = self.get_current_image()
91
-
92
- # Get expected labels for this page
93
- expected = self.page_labels.get(self.current_page, ["object"])
94
-
95
- # Build candidate labels (expected + spoken word)
96
- candidates = expected + [spoken_word]
97
- candidates = list(set(candidates)) # Remove duplicates
98
-
99
- try:
100
- # Run CLIP
101
- inputs = self.clip_processor(text=candidates, images=image, return_tensors="pt", padding=True)
102
- with torch.no_grad():
103
- outputs = self.clip_model(**inputs)
104
- logits_per_image = outputs.logits_per_image
105
- probs = logits_per_image.softmax(dim=1).numpy()
106
 
107
- # Get best match
108
- best_idx = probs[0].argmax()
109
- best_label = candidates[best_idx]
110
- confidence = float(probs[0][best_idx])
111
- except Exception as e:
112
- print(f"CLIP error: {e}")
113
- best_label = spoken_word
114
- confidence = 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- # Check if spoken word matches any expected label
117
- spoken_lower = spoken_word.lower().strip()
118
- is_correct = any(spoken_lower == exp.lower() for exp in expected)
 
 
119
 
120
- # Also check CLIP confidence for bonus
121
- clip_confirms = best_label.lower() == spoken_lower and confidence > 0.3
 
 
122
 
123
- return {
124
- "is_correct": is_correct or clip_confirms,
125
- "confidence": confidence,
126
- "detected_label": best_label,
127
- "expected_labels": expected,
128
- "spoken_word": spoken_word
129
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  def speak(self, text):
132
- """Convert text to speech and return audio file"""
133
  try:
134
  tts = gTTS(text=text, lang="en", slow=False)
135
  audio_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3").name
136
  tts.save(audio_path)
137
  return audio_path
138
- except Exception as e:
139
- print(f"TTS error: {e}")
140
  return None
141
 
142
- def get_sd_prompt(self):
143
- """Get the SD prompt for current page based on book content"""
144
- # Pages 6-37 are Tact: "What's that?"
145
- if 6 <= self.current_page <= 37:
146
- return "What's that?"
147
- # Pages 52-82 are Listener Responding: "Where is the ___?"
148
- elif 52 <= self.current_page <= 82:
149
- return "Where is the dog?"
150
- # Pages 84-108 are VP-MTS: "Match"
151
- elif 84 <= self.current_page <= 108:
152
- return "Match the picture"
153
- else:
154
- return "What is this?"
155
-
156
- def listen(self):
157
- """Capture and transcribe child's spoken response"""
158
- recognizer = sr.Recognizer()
159
- try:
160
- with sr.Microphone() as source:
161
- recognizer.adjust_for_ambient_noise(source, duration=0.5)
162
- audio = recognizer.listen(source, timeout=5, phrase_time_limit=3)
163
- text = recognizer.recognize_google(audio)
164
- return text.lower().strip()
165
- except sr.WaitTimeoutError:
166
- return "[no response]"
167
- except sr.UnknownValueError:
168
- return "[could not understand]"
169
- except Exception as e:
170
- return f"[error: {str(e)}]"
171
-
172
- def run_trial(self):
173
- """Run one complete assessment trial with CV validation"""
174
- # Get SD prompt
175
- prompt = self.get_sd_prompt()
176
- prompt_audio = self.speak(prompt)
177
 
178
- # Get current image
179
- current_image = self.get_current_image()
 
180
 
181
- # Listen to child
182
- child_response = self.listen()
 
183
 
184
- # Run CV detection
185
- cv_result = self.cv_detect_object(child_response, current_image)
 
186
 
187
- # Generate feedback
188
- if cv_result["is_correct"]:
189
- feedback = f"βœ… Great job! '{child_response}' is correct!"
 
 
 
 
 
 
 
 
 
 
 
 
190
  score = 1
191
- elif child_response == "[no response]":
192
- feedback = "⏸️ I didn't hear anything. Let's try again. " + prompt
193
- score = 0
194
- elif child_response == "[could not understand]":
195
- feedback = "🎀 I couldn't understand. Can you say it clearly?"
196
- score = 0
197
  else:
198
- expected_show = cv_result["expected_labels"][0] if cv_result["expected_labels"] else "the picture"
199
- feedback = f"❌ Not quite. I heard '{child_response}'. The answer is {expected_show}. Let's keep practicing!"
 
 
 
 
 
200
  score = 0
201
 
202
- feedback_audio = self.speak(feedback)
203
-
204
- # Log trial
205
  trial = {
206
  "timestamp": datetime.now().isoformat(),
207
  "page": self.current_page,
208
- "prompt": prompt,
209
- "child_response": child_response,
210
- "cv_detected": cv_result["detected_label"],
211
- "cv_confidence": cv_result["confidence"],
212
- "expected": cv_result["expected_labels"],
213
- "correct": cv_result["is_correct"],
214
- "score": score
215
  }
216
  self.session_data.append(trial)
217
 
218
- # Calculate totals
219
- total_score = sum(t["score"] for t in self.session_data)
220
- trials_count = len(self.session_data)
221
-
222
- return {
223
- "prompt": prompt,
224
- "prompt_audio": prompt_audio,
225
- "child_response": child_response,
226
- "cv_detected": cv_result["detected_label"],
227
- "cv_confidence": f"{cv_result['confidence']:.2%}",
228
- "feedback": feedback,
229
- "feedback_audio": feedback_audio,
230
- "score": score,
231
- "total_score": total_score,
232
- "trials_count": trials_count,
233
- "page_html": self.get_current_image_html()
234
- }
235
 
236
  def next_page(self):
237
- """Move to next assessment page"""
238
  if self.current_page < self.total_pages - 1:
239
  self.current_page += 1
240
- return self.get_current_image_html()
 
241
 
242
  def prev_page(self):
243
  """Move to previous page"""
244
  if self.current_page > 0:
245
  self.current_page -= 1
246
- return self.get_current_image_html()
 
247
 
248
  def get_session_report(self):
249
- """Generate session summary JSON"""
250
  correct_count = sum(1 for t in self.session_data if t["correct"])
251
  total = len(self.session_data)
252
  accuracy = correct_count / total if total > 0 else 0
253
 
254
- report = {
255
  "session_summary": {
256
  "total_trials": total,
257
  "correct_responses": correct_count,
@@ -261,132 +271,168 @@ class ABATutor: # FIXED: No space in class name
261
  "trials": self.session_data,
262
  "export_date": datetime.now().isoformat()
263
  }
264
- return report
265
 
266
  # ============================================
267
  # 2. INITIALIZE TUTOR
268
  # ============================================
269
 
270
- tutor = ABATutor() # FIXED: Class name without space
 
271
 
272
  # ============================================
273
  # 3. GRADIO INTERFACE
274
  # ============================================
275
 
276
  custom_css = """
277
- .green-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
278
  .main-container { max-width: 1400px; margin: auto; }
279
  .page-card { background: white; border-radius: 20px; padding: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); }
280
- .score-badge { font-size: 36px; font-weight: bold; background: #4CAF50; color: white; border-radius: 50%; width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; }
281
  .feedback-box { background: #f0f4ff; border-radius: 15px; padding: 15px; margin: 10px 0; }
 
 
282
  """
283
 
284
  with gr.Blocks(css=custom_css, title="ABA-AI-Tutor", theme=gr.themes.Soft()) as demo:
285
  gr.Markdown("""
286
- # πŸ—£οΈπŸ€– ABA-AI-Tutor
287
 
288
- ### AI-Powered VB-MAPP Level 2 Assessment
289
- *The AI shows a page, asks the question, listens to your answer, and uses computer vision to check if you're correct.*
290
  """)
291
 
292
  with gr.Row(elem_classes=["main-container"]):
293
- # LEFT COLUMN: PDF Viewer
294
  with gr.Column(scale=3):
295
  with gr.Group(elem_classes=["page-card"]):
296
- pdf_display = gr.HTML(value=tutor.get_current_image_html(), label="Current Page")
297
  with gr.Row():
298
  prev_btn = gr.Button("β—€ Previous Page", size="sm", variant="secondary")
299
- page_info = gr.Textbox(value=f"Page {tutor.current_page + 1} / {tutor.total_pages}", interactive=False, label="Page", container=False)
300
  next_btn = gr.Button("Next Page β–Ά", size="sm", variant="secondary")
301
 
302
  # RIGHT COLUMN: Assessment Controls
303
  with gr.Column(scale=2):
304
- gr.Markdown("### 🎯 Assessment Session")
 
 
 
305
 
306
- assess_btn = gr.Button("πŸŽ™οΈ Ask Question & Listen", variant="primary", size="lg")
 
 
 
 
 
 
307
 
308
  with gr.Group(elem_classes=["feedback-box"]):
309
- prompt_display = gr.Textbox(label="πŸ“’ Question Asked", interactive=False)
310
- prompt_audio = gr.Audio(label="πŸ”Š Question Audio", type="filepath", interactive=False)
311
-
312
- child_response = gr.Textbox(label="🎀 Child's Response", interactive=False, placeholder="The AI will transcribe your answer...")
313
- cv_detected = gr.Textbox(label="πŸ€– CV Detected", interactive=False, placeholder="Computer vision sees...")
314
- cv_confidence = gr.Textbox(label="πŸ“Š CV Confidence", interactive=False)
315
-
316
  feedback_msg = gr.Textbox(label="πŸ’¬ AI Feedback", interactive=False)
317
  feedback_audio = gr.Audio(label="πŸ”Š Feedback Audio", type="filepath", interactive=False)
318
 
319
  with gr.Row():
320
- trial_score = gr.Number(label="⭐ This Trial Score", interactive=False, value=0)
321
  total_score_display = gr.Number(label="πŸ† Total Score", interactive=False, value=0)
322
  trials_count = gr.Number(label="πŸ“‹ Trials Completed", interactive=False, value=0)
 
 
 
 
 
 
 
 
 
 
323
 
324
  # Session Report Tab
325
  with gr.Tabs():
326
  with gr.TabItem("πŸ“Š Session Report"):
327
  refresh_btn = gr.Button("Refresh Report")
328
  report_json = gr.JSON(label="Full Assessment Data", value=tutor.get_session_report())
329
- gr.Markdown("### πŸŽ“ Summary for Clinician")
330
  accuracy_display = gr.Markdown("**Accuracy:** 0%")
331
-
332
- with gr.TabItem("βš™οΈ Manual Navigation"):
333
- gr.Markdown("## Jump to Specific Page")
334
- page_slider = gr.Slider(minimum=0, maximum=tutor.total_pages-1, step=1, label="Page Number", value=tutor.current_page)
335
- jump_btn = gr.Button("Go to Page")
336
- page_preview = gr.HTML()
337
-
338
- def jump_to_page(page_num):
339
- tutor.current_page = int(page_num)
340
- return tutor.get_current_image_html()
341
-
342
- jump_btn.click(fn=jump_to_page, inputs=page_slider, outputs=page_preview)
343
 
344
  # ============================================
345
  # 4. EVENT HANDLERS
346
  # ============================================
347
 
348
- def run_and_update():
349
- result = tutor.run_trial()
350
- return (
351
- result["prompt"], result["prompt_audio"],
352
- result["child_response"], result["cv_detected"], result["cv_confidence"],
353
- result["feedback"], result["feedback_audio"],
354
- result["score"], result["total_score"], result["trials_count"],
355
- result["page_html"]
356
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
 
358
- assess_btn.click(
359
- fn=run_and_update,
360
- outputs=[
361
- prompt_display, prompt_audio,
362
- child_response, cv_detected, cv_confidence,
363
- feedback_msg, feedback_audio,
364
- trial_score, total_score_display, trials_count,
365
- pdf_display
366
- ]
367
  )
368
 
 
369
  def prev_page_update():
370
- html = tutor.prev_page()
371
- return html, f"Page {tutor.current_page + 1} / {tutor.total_pages}"
372
 
373
  def next_page_update():
374
- html = tutor.next_page()
375
- return html, f"Page {tutor.current_page + 1} / {tutor.total_pages}"
376
 
377
- prev_btn.click(fn=prev_page_update, outputs=[pdf_display, page_info])
378
- next_btn.click(fn=next_page_update, outputs=[pdf_display, page_info])
 
 
 
 
 
 
 
379
 
 
380
  def refresh_report():
381
  report = tutor.get_session_report()
382
  accuracy = report["session_summary"]["accuracy"]
383
  return report, f"**Accuracy:** {accuracy}"
384
 
385
  refresh_btn.click(fn=refresh_report, outputs=[report_json, accuracy_display])
386
-
387
- # ============================================
388
- # 5. LAUNCH
389
- # ============================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
  if __name__ == "__main__":
392
  demo.launch()
 
4
  import os
5
  import base64
6
  from gtts import gTTS
7
+ from ultralytics import YOLO
8
+ from PIL import Image, ImageDraw, ImageColor
 
 
9
  import io
10
  import numpy as np
11
  import json
12
  from datetime import datetime
13
+ import random
14
 
15
  # ============================================
16
+ # 1. LOAD PDF AND YOLO MODEL
17
  # ============================================
18
 
19
+ PDF_PATH = "2VBMAPP.pdf"
20
 
21
+ class ABATutor:
22
  def __init__(self):
23
+ # Load YOLOv8 model
24
+ try:
25
+ self.yolo_model = YOLO("yolov8n.pt")
26
+ print("βœ… YOLOv8 model loaded")
27
+ except Exception as e:
28
+ print(f"⚠️ YOLO error: {e}")
29
+ self.yolo_model = None
30
 
31
  # Load PDF
32
  try:
 
34
  self.total_pages = len(self.doc)
35
  print(f"βœ… Loaded PDF with {self.total_pages} pages")
36
  except Exception as e:
37
+ print(f"⚠️ PDF error: {e}")
38
  self.doc = None
39
  self.total_pages = 1
40
 
41
+ self.current_page = 6
 
 
42
  self.session_data = []
43
+ self.current_target = None # Current object the AI is asking for
44
+ self.current_detections = [] # Current YOLO detections on this page
45
+ self.current_image = None # Store current image for annotations
46
 
47
+ # Target objects for each page (from VB-MAPP book)
48
+ self.page_targets = {
 
 
 
 
 
 
 
49
  6: ["cat", "dog", "ball", "shoe", "car", "apple", "bird", "fish", "hat", "cup"],
50
  7: ["cat", "dog", "ball", "shoe", "car", "apple"],
51
  8: ["bird", "fish", "hat", "cup", "book", "chair"],
52
  9: ["train", "plane", "boat", "truck", "bus", "bike"],
53
  10: ["frog", "monkey", "elephant", "lion", "giraffe", "zebra"],
 
54
  }
55
 
56
+ def get_page_image_with_boxes(self, show_boxes=False):
57
+ """Extract current page and optionally draw YOLO detection boxes"""
58
  if self.doc is None:
 
59
  return Image.new('RGB', (800, 600), color='white')
60
 
61
  page = self.doc.load_page(self.current_page)
62
+ pix = page.get_pixmap(dpi=120)
63
  img_data = pix.tobytes("png")
64
  pil_img = Image.open(io.BytesIO(img_data))
 
 
 
 
 
 
65
 
66
+ # Run YOLO to get detections
67
+ if self.yolo_model:
68
+ results = self.yolo_model(pil_img, conf=0.25, verbose=False)
69
+ self.current_detections = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ for r in results:
72
+ if r.boxes is not None:
73
+ for box in r.boxes:
74
+ class_id = int(box.cls[0])
75
+ class_name = self.yolo_model.names[class_id].lower()
76
+ confidence = float(box.conf[0])
77
+ x1, y1, x2, y2 = box.xyxy[0].tolist()
78
+
79
+ self.current_detections.append({
80
+ "class": class_name,
81
+ "confidence": confidence,
82
+ "bbox": [int(x1), int(y1), int(x2), int(y2)],
83
+ "center": [(int(x1)+int(x2))//2, (int(y1)+int(y2))//2]
84
+ })
85
+
86
+ # Draw boxes if requested
87
+ if show_boxes:
88
+ draw = ImageDraw.Draw(pil_img)
89
+ for det in self.current_detections:
90
+ x1, y1, x2, y2 = det["bbox"]
91
+ draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
92
+ draw.text((x1, y1-15), f"{det['class']} ({det['confidence']:.0%})", fill="red")
93
 
94
+ return pil_img
95
+
96
+ def get_current_image_html(self, show_boxes=False):
97
+ """Get HTML image with clickable areas"""
98
+ pil_img = self.get_page_image_with_boxes(show_boxes=show_boxes)
99
 
100
+ # Convert to base64
101
+ buffered = io.BytesIO()
102
+ pil_img.save(buffered, format="PNG")
103
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
104
 
105
+ # If we have detections, create an image map for clickable areas
106
+ if self.current_detections:
107
+ # Build clickable areas HTML
108
+ areas_html = ""
109
+ for i, det in enumerate(self.current_detections):
110
+ x1, y1, x2, y2 = det["bbox"]
111
+ areas_html += f'''
112
+ <area shape="rect" coords="{x1},{y1},{x2},{y2}"
113
+ data-class="{det['class']}"
114
+ data-index="{i}"
115
+ onclick="selectObject(this)"
116
+ style="cursor:pointer;"
117
+ title="Click on {det['class']}">
118
+ '''
119
+
120
+ # JavaScript for click handling
121
+ js_script = """
122
+ <script>
123
+ function selectObject(area) {
124
+ let className = area.getAttribute('data-class');
125
+ let index = area.getAttribute('data-index');
126
+
127
+ // Send to Gradio
128
+ const event = new CustomEvent('gradio_click', {
129
+ detail: { class: className, index: index }
130
+ });
131
+ window.dispatchEvent(event);
132
+
133
+ // Also update a hidden input
134
+ let hiddenInput = document.getElementById('selected_object');
135
+ if (hiddenInput) {
136
+ hiddenInput.value = className;
137
+ hiddenInput.dispatchEvent(new Event('change'));
138
+ }
139
+
140
+ // Visual feedback
141
+ area.style.outline = '3px solid green';
142
+ setTimeout(() => { area.style.outline = ''; }, 500);
143
+ }
144
+ </script>
145
+ """
146
+
147
+ html = f'''
148
+ <div style="position: relative;">
149
+ <img src="data:image/png;base64,{img_base64}"
150
+ usemap="#objectmap"
151
+ style="max-width:100%; border-radius:10px; cursor:pointer;" />
152
+ <map name="objectmap" id="objectmap">
153
+ {areas_html}
154
+ </map>
155
+ <input type="hidden" id="selected_object" value="">
156
+ {js_script}
157
+ </div>
158
+ <p style="font-size:12px; color:#666; margin-top:5px;">
159
+ πŸ’‘ Click directly on any object in the image above to answer!
160
+ </p>
161
+ '''
162
+ return html
163
+ else:
164
+ # No detections found
165
+ return f'''
166
+ <img src="data:image/png;base64,{img_base64}" style="max-width:100%; border-radius:10px;" />
167
+ <p style="font-size:12px; color:#999; margin-top:5px;">
168
+ ⚠️ No objects detected on this page. Try a different page with clear pictures.
169
+ </p>
170
+ '''
171
+
172
+ def get_clickable_image(self):
173
+ """Return clickable image for Gradio"""
174
+ return self.get_current_image_html(show_boxes=True)
175
 
176
  def speak(self, text):
177
+ """Convert text to speech"""
178
  try:
179
  tts = gTTS(text=text, lang="en", slow=False)
180
  audio_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3").name
181
  tts.save(audio_path)
182
  return audio_path
183
+ except:
 
184
  return None
185
 
186
+ def ask_new_question(self):
187
+ """Generate a new question based on current page detections"""
188
+ if not self.current_detections:
189
+ return None, "No objects detected on this page. Please go to a page with clear pictures."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ # Filter detections that are in our target list for this page
192
+ valid_targets = self.page_targets.get(self.current_page, [])
193
+ eligible_detections = [d for d in self.current_detections if d["class"] in valid_targets]
194
 
195
+ if not eligible_detections:
196
+ # Use any detection if none match the page targets
197
+ eligible_detections = self.current_detections
198
 
199
+ # Pick a random object to ask about
200
+ target = random.choice(eligible_detections)
201
+ self.current_target = target
202
 
203
+ # Create the question
204
+ question = f"Where is the {target['class']}? Click on it!"
205
+ question_audio = self.speak(question)
206
+
207
+ return question, question_audio
208
+
209
+ def check_answer(self, clicked_class):
210
+ """Check if the clicked object matches what was asked"""
211
+ if not self.current_target:
212
+ return "⚠️ Please click 'Ask Question' first!", False, 0
213
+
214
+ is_correct = (clicked_class == self.current_target["class"])
215
+
216
+ if is_correct:
217
+ feedback = f"βœ… Correct! That IS the {self.current_target['class']}! Great job!"
218
  score = 1
 
 
 
 
 
 
219
  else:
220
+ # Find where the target actually is
221
+ target_pos = ""
222
+ for det in self.current_detections:
223
+ if det["class"] == self.current_target["class"]:
224
+ target_pos = f" (Look for the {det['class']} in the picture)"
225
+ break
226
+ feedback = f"❌ Not quite. You clicked on {clicked_class}, but I asked for the {self.current_target['class']}.{target_pos} Try again!"
227
  score = 0
228
 
229
+ # Log the trial
 
 
230
  trial = {
231
  "timestamp": datetime.now().isoformat(),
232
  "page": self.current_page,
233
+ "question": f"Where is the {self.current_target['class']}?",
234
+ "asked_for": self.current_target["class"],
235
+ "clicked_on": clicked_class,
236
+ "correct": is_correct,
237
+ "score": score,
238
+ "detection_confidence": self.current_target["confidence"]
 
239
  }
240
  self.session_data.append(trial)
241
 
242
+ return feedback, score, is_correct
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  def next_page(self):
245
+ """Move to next page and reset state"""
246
  if self.current_page < self.total_pages - 1:
247
  self.current_page += 1
248
+ self.current_target = None
249
+ return self.get_clickable_image(), f"Page {self.current_page + 1} / {self.total_pages}"
250
 
251
  def prev_page(self):
252
  """Move to previous page"""
253
  if self.current_page > 0:
254
  self.current_page -= 1
255
+ self.current_target = None
256
+ return self.get_clickable_image(), f"Page {self.current_page + 1} / {self.total_pages}"
257
 
258
  def get_session_report(self):
259
+ """Generate session summary"""
260
  correct_count = sum(1 for t in self.session_data if t["correct"])
261
  total = len(self.session_data)
262
  accuracy = correct_count / total if total > 0 else 0
263
 
264
+ return {
265
  "session_summary": {
266
  "total_trials": total,
267
  "correct_responses": correct_count,
 
271
  "trials": self.session_data,
272
  "export_date": datetime.now().isoformat()
273
  }
 
274
 
275
  # ============================================
276
  # 2. INITIALIZE TUTOR
277
  # ============================================
278
 
279
+ tutor = ABATutor()
280
+ selected_class_state = gr.State("")
281
 
282
  # ============================================
283
  # 3. GRADIO INTERFACE
284
  # ============================================
285
 
286
  custom_css = """
 
287
  .main-container { max-width: 1400px; margin: auto; }
288
  .page-card { background: white; border-radius: 20px; padding: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); }
 
289
  .feedback-box { background: #f0f4ff; border-radius: 15px; padding: 15px; margin: 10px 0; }
290
+ .question-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 15px; padding: 20px; }
291
+ .click-hint { background: #e8f5e9; padding: 10px; border-radius: 10px; text-align: center; font-size: 14px; }
292
  """
293
 
294
  with gr.Blocks(css=custom_css, title="ABA-AI-Tutor", theme=gr.themes.Soft()) as demo:
295
  gr.Markdown("""
296
+ # πŸ–±οΈπŸ€– ABA-AI-Tutor
297
 
298
+ ### Click-to-Select VB-MAPP Assessment
299
+ *The AI asks "Where is the ___?" You **click on the object** in the picture to answer!*
300
  """)
301
 
302
  with gr.Row(elem_classes=["main-container"]):
303
+ # LEFT COLUMN: Clickable PDF Viewer
304
  with gr.Column(scale=3):
305
  with gr.Group(elem_classes=["page-card"]):
306
+ pdf_display = gr.HTML(value=tutor.get_clickable_image())
307
  with gr.Row():
308
  prev_btn = gr.Button("β—€ Previous Page", size="sm", variant="secondary")
309
+ page_info = gr.Textbox(value=f"Page {tutor.current_page + 1} / {tutor.total_pages}", interactive=False, container=False)
310
  next_btn = gr.Button("Next Page β–Ά", size="sm", variant="secondary")
311
 
312
  # RIGHT COLUMN: Assessment Controls
313
  with gr.Column(scale=2):
314
+ gr.Markdown("### 🎯 Click Assessment")
315
+
316
+ # Hidden input to capture clicks (workaround for Gradio)
317
+ clicked_class = gr.Textbox(visible=False, elem_id="selected_object", label="Clicked Object")
318
 
319
+ with gr.Group(elem_classes=["question-card"]):
320
+ question_display = gr.Textbox(label="πŸ“’ AI Question", interactive=False, placeholder="Click 'Ask Question' to start...")
321
+ question_audio = gr.Audio(label="πŸ”Š Question Audio", type="filepath", interactive=False)
322
+
323
+ with gr.Row():
324
+ ask_btn = gr.Button("🎲 Ask New Question", variant="primary", size="lg")
325
+ check_btn = gr.Button("βœ… Check My Click", variant="secondary", size="lg")
326
 
327
  with gr.Group(elem_classes=["feedback-box"]):
 
 
 
 
 
 
 
328
  feedback_msg = gr.Textbox(label="πŸ’¬ AI Feedback", interactive=False)
329
  feedback_audio = gr.Audio(label="πŸ”Š Feedback Audio", type="filepath", interactive=False)
330
 
331
  with gr.Row():
332
+ trial_score = gr.Number(label="⭐ Last Score", interactive=False, value=0)
333
  total_score_display = gr.Number(label="πŸ† Total Score", interactive=False, value=0)
334
  trials_count = gr.Number(label="πŸ“‹ Trials Completed", interactive=False, value=0)
335
+
336
+ gr.Markdown("""
337
+ <div class="click-hint">
338
+ πŸ–±οΈ <strong>How to play:</strong><br>
339
+ 1. Click "Ask New Question"<br>
340
+ 2. Listen to what the AI asks for<br>
341
+ 3. <strong>Click directly on that object</strong> in the picture<br>
342
+ 4. Click "Check My Click" to see if you're right!
343
+ </div>
344
+ """)
345
 
346
  # Session Report Tab
347
  with gr.Tabs():
348
  with gr.TabItem("πŸ“Š Session Report"):
349
  refresh_btn = gr.Button("Refresh Report")
350
  report_json = gr.JSON(label="Full Assessment Data", value=tutor.get_session_report())
 
351
  accuracy_display = gr.Markdown("**Accuracy:** 0%")
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  # ============================================
354
  # 4. EVENT HANDLERS
355
  # ============================================
356
 
357
+ # Ask new question
358
+ def ask_question():
359
+ question, audio = tutor.ask_new_question()
360
+ if question:
361
+ return question, audio, "Waiting for your click...", None, 0, 0, 0
362
+ else:
363
+ return "No objects detected on this page. Try a different page.", None, "⚠️ No clickable objects found", None, 0, 0, 0
364
+
365
+ ask_btn.click(
366
+ fn=ask_question,
367
+ outputs=[question_display, question_audio, feedback_msg, feedback_audio, trial_score, total_score_display, trials_count]
368
+ )
369
+
370
+ # Check answer when child clicks
371
+ def process_click_and_check(clicked, current_question):
372
+ if not clicked or clicked == "":
373
+ return "Click on an object in the picture first!", None, 0, 0, 0
374
+
375
+ feedback, score, is_correct = tutor.check_answer(clicked.lower())
376
+ feedback_audio = tutor.speak(feedback)
377
+
378
+ total_score = sum(t["score"] for t in tutor.session_data)
379
+ trials = len(tutor.session_data)
380
+
381
+ return feedback, feedback_audio, score, total_score, trials
382
 
383
+ check_btn.click(
384
+ fn=process_click_and_check,
385
+ inputs=[clicked_class, question_display],
386
+ outputs=[feedback_msg, feedback_audio, trial_score, total_score_display, trials_count]
 
 
 
 
 
387
  )
388
 
389
+ # Navigation
390
  def prev_page_update():
391
+ html, info = tutor.prev_page()
392
+ return html, info, "", None, "", None, 0, 0, 0
393
 
394
  def next_page_update():
395
+ html, info = tutor.next_page()
396
+ return html, info, "", None, "", None, 0, 0, 0
397
 
398
+ prev_btn.click(
399
+ fn=prev_page_update,
400
+ outputs=[pdf_display, page_info, question_display, question_audio, feedback_msg, feedback_audio, trial_score, total_score_display, trials_count]
401
+ )
402
+
403
+ next_btn.click(
404
+ fn=next_page_update,
405
+ outputs=[pdf_display, page_info, question_display, question_audio, feedback_msg, feedback_audio, trial_score, total_score_display, trials_count]
406
+ )
407
 
408
+ # Refresh report
409
  def refresh_report():
410
  report = tutor.get_session_report()
411
  accuracy = report["session_summary"]["accuracy"]
412
  return report, f"**Accuracy:** {accuracy}"
413
 
414
  refresh_btn.click(fn=refresh_report, outputs=[report_json, accuracy_display])
415
+
416
+ # JavaScript to capture clicks and send to Gradio
417
+ demo.load(_js="""
418
+ function captureClicks() {
419
+ window.addEventListener('gradio_click', function(e) {
420
+ let hiddenInput = document.getElementById('selected_object');
421
+ if (hiddenInput) {
422
+ hiddenInput.value = e.detail.class;
423
+ hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
424
+
425
+ // Also try to update Gradio's state
426
+ const checkBtn = document.querySelector('button[aria-label="Check My Click"]');
427
+ if (checkBtn) {
428
+ checkBtn.style.animation = 'pulse 0.5s';
429
+ setTimeout(() => { checkBtn.style.animation = ''; }, 500);
430
+ }
431
+ }
432
+ });
433
+ }
434
+ captureClicks();
435
+ """)
436
 
437
  if __name__ == "__main__":
438
  demo.launch()