Purpose: This document provides technical context for a reasoning AI to generate a research paper. It focuses exclusively on ECE and AI logic—no auth or routing boilerplate.
- Name: EduSync
- Core Concept: Edge-deployed Learning Management System (LMS) using:
- Local Llama-3 (GPU) for quiz, summary, and flashcard generation
- MediaPipe (CPU) for vision-based head pose estimation during quizzes
- EasyOCR (CPU) for text extraction from PDFs/images
- Key Innovation:
- Split-Compute Architecture: Vision runs on CPU to leave GPU free for Llama-3; both pipelines execute on the same edge node.
- Multimodal Engagement Tracking: Vision (head pose during quizzes) + Behavior (scroll velocity during material viewing). Both streams logged with timestamps and user identifiers for correlation analysis.
- Objective 1 — Edge AI performance and vision-based attention: Demonstrate measurable inference performance (latency, throughput) for OCR and LLM operations, and vision-based head-pose attention monitoring with summary statistics (Table 2) and time-series/distribution figures.
- Objective 2 — Multimodal engagement and correlation with quiz performance: Demonstrate that scroll engagement (DSP) and vision-based attention (focused ratio) correlate with quiz score across users/sessions; show scatter plots with Pearson r and p-values (N ≥ 20–30 pairs).
All under backend/research_data/figures/ (generated by backend/scripts/visualize_research_data.py):
| Path | Caption |
|---|---|
engagement_vs_quiz_scatter.png |
Scatter plot of average engagement score vs quiz score with linear fit and Pearson r. |
engagement_distribution.png |
Histogram of engagement scores across sessions. |
latency_by_operation.png |
Bar chart of latency (avg or p50 ms) per operation (OCR, summary, flashcards, quiz). |
attention_timeseries.png |
Head pose (yaw and pitch) vs time for a sample quiz session. |
attention_distribution.png |
Histograms of yaw, pitch, focused ratio per student, and attention state distribution. |
attention_vs_quiz_scatter.png |
Scatter plot of focused ratio vs quiz score with linear fit and Pearson r. |
The edge node runs two heavy workloads simultaneously:
- LLM Pipeline (GPU): Llama-3 8B Q4 quantized handles quiz/summary/flashcard generation. GPU is fully allocated (
n_gpu_layers=-1). - Vision Pipeline (CPU): MediaPipe Face Mesh + OpenCV solvePnP for head pose. Runs on CPU to avoid GPU contention.
- OCR Pipeline (CPU): EasyOCR extracts text from documents. No GPU used.
Result: Students can take quizzes (vision attention monitoring) while the teacher triggers AI content generation without resource conflicts.
- Target: Laptop edge node (e.g., RTX 3060 6GB + Ryzen 7 5800H)
- Llama-3 load:
n_ctx=4096,n_batch=512,use_mlock=True,n_threads=8 - Vision: MediaPipe CPU,
min_detection_confidence=0.5 - All processing local—no cloud round-trip; enabling low-latency on-device inference.
| Model | Type | Hardware | Purpose |
|---|---|---|---|
Meta-Llama-3-8B-Instruct-Q4_K_M.gguf |
Quantized (Q4_K_M) | GPU | Quiz, summary, flashcards |
| MediaPipe Face Mesh | Pretrained | CPU | 2D face landmarks for head pose |
| EasyOCR (en) | Pretrained | CPU | OCR for PDFs/images |
Per-student buffer of last N frames (default 10). Thresholds applied to averaged yaw/pitch to reduce false positives from single-frame noise.
class HeadPoseSmoother:
def update(self, yaw: float, pitch: float, student_id: str) -> dict:
buf = self._buffers[student_id]
buf.push(yaw, pitch)
n = len(buf.yaw_buf)
yaw_avg = sum(buf.yaw_buf) / n if n else yaw
pitch_avg = sum(buf.pitch_buf) / n if n else pitch
if abs(yaw_avg) > YAW_THRESHOLD or abs(pitch_avg) > PITCH_THRESHOLD:
attention_score = 0 # distracted
else:
attention_score = 100 # focused
return {"yaw_avg": yaw_avg, "pitch_avg": pitch_avg, "attention_score": attention_score, ...}6-point 3D face model (nose, chin, eye corners, mouth corners) mapped to 2D MediaPipe landmarks. solvePnP yields rotation vector; Rodrigues + arctan2 yield yaw, pitch, roll in degrees.
# Generic 3D face model (mm)
FACE_3D_MODEL = np.array([
(0.0, 0.0, 0.0), # Nose tip
(0.0, -330.0, -65.0), # Chin
(-225.0, 170.0, -135.0), # Left eye left
(225.0, 170.0, -135.0), # Right eye right
(-150.0, -150.0, -125.0), # Left mouth
(150.0, -150.0, -125.0), # Right mouth
], dtype=np.float64)
# solvePnP
success, rvec, tvec = cv2.solvePnP(
FACE_3D_MODEL, points_2d, camera_matrix, dist_coeffs,
flags=cv2.SOLVEPNP_ITERATIVE,
)
yaw, pitch, roll = _rotation_vector_to_euler(rvec)
# Euler from rotation matrix (convention: yaw Y, pitch X, roll Z)
def _rotation_vector_to_euler(rvec):
rmat, _ = cv2.Rodrigues(rvec)
sy = np.sqrt(rmat[0,0]**2 + rmat[1,0]**2)
yaw = np.degrees(np.arctan2(rmat[1,0], rmat[0,0]))
pitch = np.degrees(np.arctan2(-rmat[2,0], sy))
roll = np.degrees(np.arctan2(rmat[2,1], rmat[2,2]))
return yaw, pitch, roll|yaw| > 20°OR|pitch| > 15°→ distracted (attention_score = 0)- Else → focused (attention_score = 100)
Applied to smoothed values when student_id is provided.
Scroll velocity sampled at 1 Hz (pixels/second). FIR moving-average filter, then band-pass classification:
- Idle: ≤ 2 px/s
- Reading: 2–100 px/s
- Skimming: > 100 px/s
# Base score from reading ratio (0–60)
base_score = reading_ratio * 60
# Energy bonus (0–25), normalized
energy_normalized = min(energy / 1000, 1.0)
energy_bonus = energy_normalized * 25
# ZCR bonus (0–15), optimal ZCR ≈ 0.3
optimal_zcr = 0.3
zcr_factor = 1 - abs(zcr - optimal_zcr) / optimal_zcr
zcr_bonus = zcr_factor * 15
# Final Scroll Engagement Score (0–100)
engagement_score = base_score + energy_bonus + zcr_bonus
engagement_score = clamp(engagement_score, 0, 100)- Vision:
attention_log.csv— per-frame head pose,student_id,timestamp_iso,attention_state(0/1). - Scroll:
engagement_metrics.jsonl— per-session scroll signal,user_id,scroll_engagement_score,timestamp. - Correlation: Join by
student_id=user_idand align by time for multimodal analysis (e.g., scatter: focused_ratio vs scroll_engagement_score).
Llama-3 Instruct format: <|start_header_id|>system<|end_header_id|>...<|eot_id|><|start_header_id|>user<|end_header_id|>...<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Quiz (5 MCQs):
prompt = f"""<|start_header_id|>system<|end_header_id|>
You are a strict JSON generator. Output exactly 5 multiple-choice questions in a list based on the text provided.
Do not include any text outside the JSON. Keys must be exactly: "question", "options", "correct_answer".
"options" must be a list of 4 strings. "correct_answer" is the index (0-3).
<|eot_id|><|start_header_id|>user<|end_header_id|>
Context: {content_text[:6000]}
<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
output = llm(prompt, max_tokens=512, temperature=0.4, stop=["<|eot_id|>"])Summary:
prompt = f"""<|start_header_id|>system<|end_header_id|>
You are an educational assistant. Summarize the following study material as key bullet points that are easy to learn.
Use concise bullet points only, not paragraphs. Cover main concepts and important details.
<|eot_id|><|start_header_id|>user<|end_header_id|>
{content_text[:8000]}
<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
output = llm(prompt, max_tokens=512, temperature=0.5, stop=["<|eot_id|>"])Flashcards:
prompt = f"""<|start_header_id|>system<|end_header_id|>
You are a strict JSON generator. Output exactly 5 valid JSON objects in a list.
Each object must have exactly: "front" (question/term) and "back" (answer/definition).
<|eot_id|><|start_header_id|>user<|end_header_id|>
Context: {content_text[:8000]}
<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
output = llm(prompt, max_tokens=512, temperature=0.4, stop=["<|eot_id|>"])Header (current/new format):
timestamp,timestamp_iso,student_id,yaw,pitch,roll,attention_state,assignment_id
Sample rows (mock + real-like):
timestamp,timestamp_iso,student_id,yaw,pitch,roll,attention_state,assignment_id
1771165319397,2026-02-14T10:21:59.397Z,6,-8.08,8.81,172.01,1,1
1771165321254,2026-02-14T10:22:01.254Z,6,-8.37,7.45,169.53,1,1
1771165323367,2026-02-14T10:22:03.367Z,6,-8.47,9.23,170.19,1,1
1771165731463,2026-02-14T10:28:51.463Z,10,-176.16,-21.19,-147.09,0,1
1771165733433,2026-02-14T10:28:53.433Z,10,151.36,-40.43,-147.73,0,1
1771165735535,2026-02-14T10:28:55.535Z,10,179.16,21.13,-156.03,0,1
1771165739475,2026-02-14T10:29:39.475Z,10,-6.48,12.24,152.97,1,1Note: attention_state 1 = focused, 0 = distracted.
Fields per line:
timestamp, operation, duration_ms, duration_s, input_chars, output_chars, chars_per_second, model, gpu_layers, platform
Sample rows (real):
{"timestamp": "2026-02-14T10:12:24.140422", "operation": "ocr", "duration_ms": 167.3, "duration_s": 0.167, "input_chars": 1769772, "output_chars": 14788, "chars_per_second": 88392.74, "model": "llama-3-8b-q4", "gpu_layers": -1, "platform": "edge"}
{"timestamp": "2026-02-14T10:15:05.325130", "operation": "summary", "duration_ms": 111667.16, "duration_s": 111.667, "input_chars": 14788, "output_chars": 1701, "chars_per_second": 15.23, "model": "llama-3-8b-q4", "gpu_layers": -1, "platform": "edge"}
{"timestamp": "2026-02-14T10:19:44.866193", "operation": "flashcards", "duration_ms": 101620.33, "duration_s": 101.62, "input_chars": 14788, "output_chars": 1226, "chars_per_second": 12.06, "model": "llama-3-8b-q4", "gpu_layers": -1, "platform": "edge"}
{"timestamp": "2026-02-14T10:21:28.201851", "operation": "quiz", "duration_ms": 99227.64, "duration_s": 99.228, "input_chars": 14788, "output_chars": 1207, "chars_per_second": 12.16, "model": "llama-3-8b-q4", "gpu_layers": -1, "platform": "edge"}Fields per line:
timestamp, user_id, material_id, engagement_score, session_duration_s, signal_length, sampling_rate_hz,
mean, std, max, min, energy, power, zero_crossings, zcr, dominant_freq_hz, spectral_centroid,
reading_ratio, idle_ratio, skimming_ratio, base_score, energy_bonus, zcr_bonus, final_score,
filter_type, filter_order, scipy_available, scroll_engagement_score
Sample rows (real):
{"timestamp": "2026-02-14T10:17:55.080668", "user_id": 3, "material_id": 1, "engagement_score": 39.73, "session_duration_s": 196, "signal_length": 196, "sampling_rate_hz": 1.0, "mean": 606.59, "std": 2205.5, "max": 26235.0, "min": 0.0, "energy": 5232205.51, "zero_crossings": 24, "zcr": 0.1231, "reading_ratio": 0.1429, "idle_ratio": 0.2806, "skimming_ratio": 0.5765, "base_score": 8.57, "energy_bonus": 25.0, "zcr_bonus": 6.15, "final_score": 39.73, "scroll_engagement_score": 39.73, "filter_type": "FIR_moving_average", "filter_order": 5}
{"timestamp": "2026-02-14T10:21:35.259098", "user_id": 3, "material_id": 1, "engagement_score": 59.66, "session_duration_s": 39, "signal_length": 39, "mean": 130.31, "reading_ratio": 0.3846, "idle_ratio": 0.1026, "skimming_ratio": 0.5128, "scroll_engagement_score": 59.66}
{"timestamp": "2026-02-14T10:35:09.782521", "user_id": 6, "material_id": 1, "engagement_score": 64.32, "session_duration_s": 451, "reading_ratio": 0.4146, "scroll_engagement_score": 64.32}
{"timestamp": "2026-02-14T10:42:00.343996", "user_id": 8, "material_id": 1, "engagement_score": 37.83, "session_duration_s": 116, "reading_ratio": 0.069, "skimming_ratio": 0.6207, "scroll_engagement_score": 37.83}
{"timestamp": "2026-02-15T14:28:39.160779", "user_id": 10, "material_id": 1, "engagement_score": 25.0, "session_duration_s": 4, "reading_ratio": 0.0, "skimming_ratio": 0.75, "scroll_engagement_score": 25.0}| Operation | duration_ms | duration_s | input_chars | output_chars | chars_per_second |
|---|---|---|---|---|---|
| ocr | 167.3 | 0.167 | 1,769,772 | 14,788 | 88,392.74 |
| summary | 111,667.16 | 111.67 | 14,788 | 1,701 | 15.23 |
| flashcards | 101,620.33 | 101.62 | 14,788 | 1,226 | 12.06 |
| quiz | 99,227.64 | 99.23 | 14,788 | 1,207 | 12.16 |
Note: OCR input is raw bytes (char count ≈ file size); LLM operations use text. chars_per_second = output_chars / duration_s.
| operation | count | avg_ms | p50_ms | p95_ms | min_ms | max_ms |
|---|---|---|---|---|---|---|
| ocr | 1 | 167.3 | 167.3 | 167.3 | 167.3 | 167.3 |
| summary | 1 | 111667.2 | 111667.2 | 111667.2 | 111667.2 | 111667.2 |
| flashcards | 1 | 101620.3 | 101620.3 | 101620.3 | 101620.3 | 101620.3 |
| quiz | 1 | 99227.6 | 99227.6 | 99227.6 | 99227.6 | 99227.6 |
| event | model_load_time_ms |
|---|---|
| model_load | ~1,454 – 5,992 |
Typical range observed: 2,300–6,000 ms depending on cold start.
- OCR: EasyOCR runs on CPU; ~88k chars/s throughput for text extraction.
- LLM: Throughput ~12–15 chars/s for quiz/summary/flashcards on edge GPU (RTX 3060 6GB class).
- Vision: MediaPipe Face Mesh runs on CPU; ~0.5 s per frame (2 Hz capture). No explicit CPU % logged; all vision logic is CPU-only.