Kexin-251202 commited on
Commit
58fed9b
·
verified ·
1 Parent(s): 58d040a

Upload 4 files

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +24 -0
  3. README.md +3 -4
  4. app.py +649 -0
  5. focus_guard.db +3 -0
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  GAP_Large_project-fea-ui/focus_guard.db filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  GAP_Large_project-fea-ui/focus_guard.db filter=lfs diff=lfs merge=lfs -text
37
+ focus_guard.db filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies for OpenCV
6
+ RUN apt-get update && apt-get install -y \
7
+ libglib2.0-0 \
8
+ libsm6 \
9
+ libxext6 \
10
+ libxrender-dev \
11
+ libgomp1 \
12
+ libgthread-2.0-0 \
13
+ libgl1 \
14
+ libglib2.0-0 \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ COPY requirements.txt .
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ COPY . .
21
+
22
+ EXPOSE 7860
23
+
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,8 +1,7 @@
1
  ---
2
- title: FGtest
3
- emoji: 🐢
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
 
1
  ---
2
+ title: FOCUS GUARD
3
+ colorFrom: green
4
+ colorTo: red
 
5
  sdk: docker
6
  pinned: false
7
  ---
app.py ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from pydantic import BaseModel
6
+ from typing import Optional, List
7
+ import base64
8
+ import cv2
9
+ import numpy as np
10
+ import aiosqlite
11
+ import json
12
+ from datetime import datetime, timedelta
13
+ import math
14
+ import os
15
+ from pathlib import Path
16
+
17
+ # Initialize FastAPI app
18
+ app = FastAPI(title="Focus Guard API")
19
+
20
+ # Add CORS middleware
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # Global variables
30
+ model = None
31
+ db_path = "focus_guard.db"
32
+
33
+ # ================ DATABASE MODELS ================
34
+
35
+ async def init_database():
36
+ """Initialize SQLite database with required tables"""
37
+ async with aiosqlite.connect(db_path) as db:
38
+ # FocusSessions table
39
+ await db.execute("""
40
+ CREATE TABLE IF NOT EXISTS focus_sessions (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ start_time TIMESTAMP NOT NULL,
43
+ end_time TIMESTAMP,
44
+ duration_seconds INTEGER DEFAULT 0,
45
+ focus_score REAL DEFAULT 0.0,
46
+ total_frames INTEGER DEFAULT 0,
47
+ focused_frames INTEGER DEFAULT 0,
48
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
49
+ )
50
+ """)
51
+
52
+ # FocusEvents table
53
+ await db.execute("""
54
+ CREATE TABLE IF NOT EXISTS focus_events (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ session_id INTEGER NOT NULL,
57
+ timestamp TIMESTAMP NOT NULL,
58
+ is_focused BOOLEAN NOT NULL,
59
+ confidence REAL NOT NULL,
60
+ detection_data TEXT,
61
+ FOREIGN KEY (session_id) REFERENCES focus_sessions (id)
62
+ )
63
+ """)
64
+
65
+ # UserSettings table
66
+ await db.execute("""
67
+ CREATE TABLE IF NOT EXISTS user_settings (
68
+ id INTEGER PRIMARY KEY CHECK (id = 1),
69
+ sensitivity INTEGER DEFAULT 6,
70
+ notification_enabled BOOLEAN DEFAULT 1,
71
+ notification_threshold INTEGER DEFAULT 30,
72
+ frame_rate INTEGER DEFAULT 30,
73
+ model_name TEXT DEFAULT 'yolov8n.pt'
74
+ )
75
+ """)
76
+
77
+ # Insert default settings if not exists
78
+ await db.execute("""
79
+ INSERT OR IGNORE INTO user_settings (id, sensitivity, notification_enabled, notification_threshold, frame_rate, model_name)
80
+ VALUES (1, 6, 1, 30, 30, 'yolov8n.pt')
81
+ """)
82
+
83
+ await db.commit()
84
+
85
+ # ================ PYDANTIC MODELS ================
86
+
87
+ class SessionCreate(BaseModel):
88
+ pass
89
+
90
+ class SessionEnd(BaseModel):
91
+ session_id: int
92
+
93
+ class SettingsUpdate(BaseModel):
94
+ sensitivity: Optional[int] = None
95
+ notification_enabled: Optional[bool] = None
96
+ notification_threshold: Optional[int] = None
97
+ frame_rate: Optional[int] = None
98
+
99
+ # ================ YOLO MODEL LOADING ================
100
+
101
+ def load_yolo_model():
102
+ """Load YOLOv8 model with optimizations for CPU"""
103
+ global model
104
+ try:
105
+ # Fix PyTorch 2.6+ weights_only issue
106
+ # Set environment variable to allow loading YOLO weights
107
+ os.environ['TORCH_LOAD_WEIGHTS_ONLY'] = '0'
108
+
109
+ import torch
110
+ if hasattr(torch.serialization, 'add_safe_globals'):
111
+ # PyTorch 2.6+ compatibility - add required classes
112
+ try:
113
+ from ultralytics.nn.tasks import DetectionModel
114
+ import torch.nn as nn
115
+ torch.serialization.add_safe_globals([
116
+ DetectionModel,
117
+ nn.modules.container.Sequential,
118
+ ])
119
+ except Exception as e:
120
+ print(f" Safe globals setup: {e}")
121
+
122
+ from ultralytics import YOLO
123
+
124
+ model_path = "models/yolov8n.pt"
125
+
126
+ # Check if model file exists, if not use yolov8n (will download)
127
+ if not os.path.exists(model_path):
128
+ print(f"Model file {model_path} not found, downloading yolov8n.pt...")
129
+ model_path = "yolov8n.pt" # This will trigger auto-download
130
+
131
+ # Load model (ultralytics handles weights_only internally in newer versions)
132
+ model = YOLO(model_path)
133
+
134
+ # Optimize for CPU
135
+ try:
136
+ model.fuse() # Fuse Conv2d + BatchNorm layers
137
+ print("[OK] Model layers fused for optimization")
138
+ except Exception as e:
139
+ print(f" Model fusion skipped: {e}")
140
+
141
+ # Warm up model with dummy inference
142
+ print("Warming up model...")
143
+ dummy_img = np.zeros((416, 416, 3), dtype=np.uint8)
144
+ model(dummy_img, imgsz=416, conf=0.4, iou=0.45, max_det=5, classes=[0], verbose=False)
145
+
146
+ print("[OK] YOLOv8 model loaded and warmed up successfully")
147
+ return True
148
+ except Exception as e:
149
+ print(f"[ERROR] Failed to load YOLOv8 model: {e}")
150
+ print(" The app will run without detection features")
151
+ import traceback
152
+ traceback.print_exc()
153
+ return False
154
+
155
+ # ================ FOCUS DETECTION ALGORITHM ================
156
+
157
+ def is_user_focused(detections, frame_shape, sensitivity=6):
158
+ """
159
+ Determine if user is focused based on YOLOv8 detections
160
+
161
+ Simple logic: Detects person with confidence >= 80% (0.8)
162
+
163
+ Args:
164
+ detections: List of detection dictionaries
165
+ frame_shape: Tuple of (height, width, channels)
166
+ sensitivity: Integer 1-10, higher = stricter criteria (adjusts confidence threshold)
167
+
168
+ Returns:
169
+ Tuple of (is_focused: bool, confidence: float, metadata: dict)
170
+ """
171
+ # Filter person detections (class 0 in COCO dataset)
172
+ persons = [d for d in detections if d.get('class') == 0]
173
+
174
+ if not persons:
175
+ return False, 0.0, {'reason': 'no_person', 'count': 0}
176
+
177
+ # Find person with highest confidence
178
+ best_person = max(persons, key=lambda x: x.get('confidence', 0))
179
+ bbox = best_person['bbox'] # [x1, y1, x2, y2]
180
+ conf = best_person['confidence']
181
+
182
+ # Calculate confidence threshold based on sensitivity
183
+ # sensitivity 6 (default) = 0.8 threshold
184
+ # sensitivity 1 (lowest) = 0.5 threshold
185
+ # sensitivity 10 (highest) = 0.9 threshold
186
+ base_threshold = 0.8
187
+ sensitivity_adjustment = (sensitivity - 6) * 0.02 # ±0.08 range
188
+ confidence_threshold = base_threshold + sensitivity_adjustment
189
+ confidence_threshold = max(0.5, min(0.95, confidence_threshold)) # Clamp to 0.5-0.95
190
+
191
+ # Simple focus determination: confidence >= threshold
192
+ is_focused = conf >= confidence_threshold
193
+
194
+ # Optional: Check if person is somewhat centered (loose requirement)
195
+ h, w = frame_shape[0], frame_shape[1]
196
+ bbox_center_x = (bbox[0] + bbox[2]) / 2
197
+ bbox_center_y = (bbox[1] + bbox[3]) / 2
198
+
199
+ # Normalize to 0-1 range
200
+ center_x_norm = bbox_center_x / w if w > 0 else 0.5
201
+ center_y_norm = bbox_center_y / h if h > 0 else 0.5
202
+
203
+ # Check if person is in frame (not at extreme edges)
204
+ # Allow very loose centering: 20%-80% horizontal, 15%-85% vertical
205
+ in_frame = (0.2 <= center_x_norm <= 0.8) and (0.15 <= center_y_norm <= 0.85)
206
+
207
+ # Reduce focus score if person is at extreme edge
208
+ position_factor = 1.0 if in_frame else 0.7
209
+ final_score = conf * position_factor
210
+
211
+ # Also reduce if multiple persons detected
212
+ if len(persons) > 1:
213
+ final_score *= 0.9
214
+ reason = f"person_detected_multi_{len(persons)}"
215
+ else:
216
+ reason = "person_detected" if is_focused else "low_confidence"
217
+
218
+ metadata = {
219
+ 'bbox': bbox,
220
+ 'detection_confidence': round(conf, 3),
221
+ 'confidence_threshold': round(confidence_threshold, 3),
222
+ 'center_position': [round(center_x_norm, 3), round(center_y_norm, 3)],
223
+ 'in_frame': in_frame,
224
+ 'person_count': len(persons),
225
+ 'reason': reason
226
+ }
227
+
228
+ return is_focused and in_frame, final_score, metadata
229
+
230
+ def parse_yolo_results(results):
231
+ """Parse YOLOv8 results into a list of detections"""
232
+ detections = []
233
+
234
+ if results and len(results) > 0:
235
+ result = results[0]
236
+ boxes = result.boxes
237
+
238
+ if boxes is not None and len(boxes) > 0:
239
+ for box in boxes:
240
+ # Get box coordinates
241
+ xyxy = box.xyxy[0].cpu().numpy()
242
+ conf = float(box.conf[0].cpu().numpy())
243
+ cls = int(box.cls[0].cpu().numpy())
244
+
245
+ detection = {
246
+ 'bbox': [float(x) for x in xyxy],
247
+ 'confidence': conf,
248
+ 'class': cls,
249
+ 'class_name': result.names[cls] if hasattr(result, 'names') else str(cls)
250
+ }
251
+ detections.append(detection)
252
+
253
+ return detections
254
+
255
+ # ================ DATABASE OPERATIONS ================
256
+
257
+ async def create_session():
258
+ """Create a new focus session"""
259
+ async with aiosqlite.connect(db_path) as db:
260
+ cursor = await db.execute(
261
+ "INSERT INTO focus_sessions (start_time) VALUES (?)",
262
+ (datetime.now().isoformat(),)
263
+ )
264
+ await db.commit()
265
+ return cursor.lastrowid
266
+
267
+ async def end_session(session_id: int):
268
+ """End a focus session and calculate statistics"""
269
+ async with aiosqlite.connect(db_path) as db:
270
+ # Get session data
271
+ cursor = await db.execute(
272
+ "SELECT start_time, total_frames, focused_frames FROM focus_sessions WHERE id = ?",
273
+ (session_id,)
274
+ )
275
+ row = await cursor.fetchone()
276
+
277
+ if not row:
278
+ return None
279
+
280
+ start_time_str, total_frames, focused_frames = row
281
+ start_time = datetime.fromisoformat(start_time_str)
282
+ end_time = datetime.now()
283
+ duration = (end_time - start_time).total_seconds()
284
+
285
+ # Calculate focus score
286
+ focus_score = focused_frames / total_frames if total_frames > 0 else 0.0
287
+
288
+ # Update session
289
+ await db.execute("""
290
+ UPDATE focus_sessions
291
+ SET end_time = ?, duration_seconds = ?, focus_score = ?
292
+ WHERE id = ?
293
+ """, (end_time.isoformat(), int(duration), focus_score, session_id))
294
+
295
+ await db.commit()
296
+
297
+ return {
298
+ 'session_id': session_id,
299
+ 'start_time': start_time_str,
300
+ 'end_time': end_time.isoformat(),
301
+ 'duration_seconds': int(duration),
302
+ 'focus_score': round(focus_score, 3),
303
+ 'total_frames': total_frames,
304
+ 'focused_frames': focused_frames
305
+ }
306
+
307
+ async def store_focus_event(session_id: int, is_focused: bool, confidence: float, metadata: dict):
308
+ """Store a focus detection event"""
309
+ async with aiosqlite.connect(db_path) as db:
310
+ await db.execute("""
311
+ INSERT INTO focus_events (session_id, timestamp, is_focused, confidence, detection_data)
312
+ VALUES (?, ?, ?, ?, ?)
313
+ """, (session_id, datetime.now().isoformat(), is_focused, confidence, json.dumps(metadata)))
314
+
315
+ # Update session frame counts
316
+ await db.execute(f"""
317
+ UPDATE focus_sessions
318
+ SET total_frames = total_frames + 1,
319
+ focused_frames = focused_frames + {1 if is_focused else 0}
320
+ WHERE id = ?
321
+ """, (session_id,))
322
+
323
+ await db.commit()
324
+
325
+ # ================ STARTUP/SHUTDOWN EVENTS ================
326
+
327
+ @app.on_event("startup")
328
+ async def startup_event():
329
+ """Initialize database and load model on startup"""
330
+ print(" Starting Focus Guard API...")
331
+ await init_database()
332
+ print("[OK] Database initialized")
333
+ load_yolo_model()
334
+
335
+ @app.on_event("shutdown")
336
+ async def shutdown_event():
337
+ """Cleanup on shutdown"""
338
+ print(" Shutting down Focus Guard API...")
339
+
340
+ # ================ STATIC FILES ================
341
+
342
+ app.mount("/static", StaticFiles(directory="static"), name="static")
343
+
344
+ @app.get("/")
345
+ async def read_index():
346
+ return FileResponse("static/index.html")
347
+
348
+ # ================ WEBSOCKET ENDPOINT ================
349
+
350
+ @app.websocket("/ws/video")
351
+ async def websocket_endpoint(websocket: WebSocket):
352
+ await websocket.accept()
353
+ session_id = None
354
+ frame_count = 0
355
+ last_inference_time = 0
356
+ min_inference_interval = 0.1 # Max 10 FPS server-side
357
+
358
+ try:
359
+ # Get user settings
360
+ async with aiosqlite.connect(db_path) as db:
361
+ cursor = await db.execute("SELECT sensitivity FROM user_settings WHERE id = 1")
362
+ row = await cursor.fetchone()
363
+ sensitivity = row[0] if row else 6
364
+
365
+ while True:
366
+ # Receive data from client
367
+ data = await websocket.receive_json()
368
+
369
+ if data['type'] == 'frame':
370
+ from time import time
371
+ current_time = time()
372
+
373
+ # Rate limiting
374
+ if current_time - last_inference_time < min_inference_interval:
375
+ # Skip inference, just acknowledge
376
+ await websocket.send_json({
377
+ 'type': 'ack',
378
+ 'frame_count': frame_count
379
+ })
380
+ continue
381
+
382
+ last_inference_time = current_time
383
+
384
+ try:
385
+ # Decode base64 image
386
+ img_data = base64.b64decode(data['image'])
387
+ nparr = np.frombuffer(img_data, np.uint8)
388
+ frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
389
+
390
+ if frame is None:
391
+ continue
392
+
393
+ # Resize for faster inference
394
+ frame = cv2.resize(frame, (640, 480))
395
+
396
+ # YOLOv8 inference
397
+ if model is not None:
398
+ results = model(
399
+ frame,
400
+ imgsz=416,
401
+ conf=0.4,
402
+ iou=0.45,
403
+ max_det=5,
404
+ classes=[0], # Only person class
405
+ verbose=False
406
+ )
407
+ detections = parse_yolo_results(results)
408
+ else:
409
+ # Fallback if model not loaded
410
+ detections = []
411
+
412
+ # Determine focus status
413
+ is_focused, confidence, metadata = is_user_focused(
414
+ detections, frame.shape, sensitivity
415
+ )
416
+
417
+ # Store event in database if session active
418
+ if session_id:
419
+ await store_focus_event(session_id, is_focused, confidence, metadata)
420
+
421
+ # Send results back to client
422
+ response = {
423
+ 'type': 'detection',
424
+ 'focused': is_focused,
425
+ 'confidence': round(confidence, 3),
426
+ 'detections': detections,
427
+ 'frame_count': frame_count
428
+ }
429
+
430
+ await websocket.send_json(response)
431
+ frame_count += 1
432
+
433
+ except Exception as e:
434
+ print(f"Error processing frame: {e}")
435
+ await websocket.send_json({
436
+ 'type': 'error',
437
+ 'message': str(e)
438
+ })
439
+
440
+ elif data['type'] == 'start_session':
441
+ session_id = await create_session()
442
+ await websocket.send_json({
443
+ 'type': 'session_started',
444
+ 'session_id': session_id
445
+ })
446
+
447
+ elif data['type'] == 'end_session':
448
+ if session_id:
449
+ summary = await end_session(session_id)
450
+ await websocket.send_json({
451
+ 'type': 'session_ended',
452
+ 'summary': summary
453
+ })
454
+ session_id = None
455
+
456
+ except WebSocketDisconnect:
457
+ if session_id:
458
+ await end_session(session_id)
459
+ print(f"WebSocket disconnected (session: {session_id})")
460
+ except Exception as e:
461
+ print(f"WebSocket error: {e}")
462
+ if websocket.client_state.value == 1: # CONNECTED
463
+ await websocket.close()
464
+
465
+ # ================ REST API ENDPOINTS ================
466
+
467
+ @app.post("/api/sessions/start")
468
+ async def api_start_session():
469
+ """Start a new focus session"""
470
+ session_id = await create_session()
471
+ return {"session_id": session_id}
472
+
473
+ @app.post("/api/sessions/end")
474
+ async def api_end_session(data: SessionEnd):
475
+ """End a focus session"""
476
+ summary = await end_session(data.session_id)
477
+ if not summary:
478
+ raise HTTPException(status_code=404, detail="Session not found")
479
+ return summary
480
+
481
+ @app.get("/api/sessions")
482
+ async def get_sessions(filter: str = "all", limit: int = 50, offset: int = 0):
483
+ """Get focus sessions with optional filtering"""
484
+ async with aiosqlite.connect(db_path) as db:
485
+ db.row_factory = aiosqlite.Row
486
+
487
+ # Build query based on filter
488
+ if filter == "today":
489
+ date_filter = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
490
+ query = "SELECT * FROM focus_sessions WHERE start_time >= ? ORDER BY start_time DESC LIMIT ? OFFSET ?"
491
+ params = (date_filter.isoformat(), limit, offset)
492
+ elif filter == "week":
493
+ date_filter = datetime.now() - timedelta(days=7)
494
+ query = "SELECT * FROM focus_sessions WHERE start_time >= ? ORDER BY start_time DESC LIMIT ? OFFSET ?"
495
+ params = (date_filter.isoformat(), limit, offset)
496
+ elif filter == "month":
497
+ date_filter = datetime.now() - timedelta(days=30)
498
+ query = "SELECT * FROM focus_sessions WHERE start_time >= ? ORDER BY start_time DESC LIMIT ? OFFSET ?"
499
+ params = (date_filter.isoformat(), limit, offset)
500
+ else:
501
+ query = "SELECT * FROM focus_sessions WHERE end_time IS NOT NULL ORDER BY start_time DESC LIMIT ? OFFSET ?"
502
+ params = (limit, offset)
503
+
504
+ cursor = await db.execute(query, params)
505
+ rows = await cursor.fetchall()
506
+
507
+ sessions = [dict(row) for row in rows]
508
+ return sessions
509
+
510
+ @app.get("/api/sessions/{session_id}")
511
+ async def get_session(session_id: int):
512
+ """Get detailed session information"""
513
+ async with aiosqlite.connect(db_path) as db:
514
+ db.row_factory = aiosqlite.Row
515
+ cursor = await db.execute("SELECT * FROM focus_sessions WHERE id = ?", (session_id,))
516
+ row = await cursor.fetchone()
517
+
518
+ if not row:
519
+ raise HTTPException(status_code=404, detail="Session not found")
520
+
521
+ session = dict(row)
522
+
523
+ # Get events
524
+ cursor = await db.execute(
525
+ "SELECT * FROM focus_events WHERE session_id = ? ORDER BY timestamp",
526
+ (session_id,)
527
+ )
528
+ events = [dict(r) for r in await cursor.fetchall()]
529
+ session['events'] = events
530
+
531
+ return session
532
+
533
+ @app.get("/api/settings")
534
+ async def get_settings():
535
+ """Get user settings"""
536
+ async with aiosqlite.connect(db_path) as db:
537
+ db.row_factory = aiosqlite.Row
538
+ cursor = await db.execute("SELECT * FROM user_settings WHERE id = 1")
539
+ row = await cursor.fetchone()
540
+
541
+ if row:
542
+ return dict(row)
543
+ else:
544
+ return {
545
+ 'sensitivity': 6,
546
+ 'notification_enabled': True,
547
+ 'notification_threshold': 30,
548
+ 'frame_rate': 30,
549
+ 'model_name': 'yolov8n.pt'
550
+ }
551
+
552
+ @app.put("/api/settings")
553
+ async def update_settings(settings: SettingsUpdate):
554
+ """Update user settings"""
555
+ async with aiosqlite.connect(db_path) as db:
556
+ # First ensure the record exists
557
+ cursor = await db.execute("SELECT id FROM user_settings WHERE id = 1")
558
+ exists = await cursor.fetchone()
559
+
560
+ if not exists:
561
+ # Insert default record if it doesn't exist
562
+ await db.execute("""
563
+ INSERT INTO user_settings (id, sensitivity, notification_enabled, notification_threshold, frame_rate, model_name)
564
+ VALUES (1, 6, 1, 30, 30, 'yolov8n.pt')
565
+ """)
566
+ await db.commit()
567
+ print("[OK] Created default user_settings record")
568
+
569
+ # Now update with provided values
570
+ updates = []
571
+ params = []
572
+
573
+ if settings.sensitivity is not None:
574
+ updates.append("sensitivity = ?")
575
+ params.append(max(1, min(10, settings.sensitivity)))
576
+
577
+ if settings.notification_enabled is not None:
578
+ updates.append("notification_enabled = ?")
579
+ params.append(settings.notification_enabled)
580
+
581
+ if settings.notification_threshold is not None:
582
+ updates.append("notification_threshold = ?")
583
+ params.append(max(5, min(300, settings.notification_threshold)))
584
+
585
+ if settings.frame_rate is not None:
586
+ updates.append("frame_rate = ?")
587
+ params.append(max(5, min(60, settings.frame_rate)))
588
+
589
+ if updates:
590
+ query = f"UPDATE user_settings SET {', '.join(updates)} WHERE id = 1"
591
+ await db.execute(query, params)
592
+ await db.commit()
593
+ print(f"[OK] Settings updated: {settings.model_dump(exclude_none=True)}")
594
+
595
+ return {"status": "success", "updated": len(updates) > 0}
596
+
597
+ @app.get("/api/stats/summary")
598
+ async def get_stats_summary():
599
+ """Get overall statistics summary"""
600
+ async with aiosqlite.connect(db_path) as db:
601
+ # Total sessions
602
+ cursor = await db.execute("SELECT COUNT(*) FROM focus_sessions WHERE end_time IS NOT NULL")
603
+ total_sessions = (await cursor.fetchone())[0]
604
+
605
+ # Total focus time
606
+ cursor = await db.execute("SELECT SUM(duration_seconds) FROM focus_sessions WHERE end_time IS NOT NULL")
607
+ total_focus_time = (await cursor.fetchone())[0] or 0
608
+
609
+ # Average focus score
610
+ cursor = await db.execute("SELECT AVG(focus_score) FROM focus_sessions WHERE end_time IS NOT NULL")
611
+ avg_focus_score = (await cursor.fetchone())[0] or 0.0
612
+
613
+ # Streak calculation (consecutive days with sessions)
614
+ cursor = await db.execute("""
615
+ SELECT DISTINCT DATE(start_time) as session_date
616
+ FROM focus_sessions
617
+ WHERE end_time IS NOT NULL
618
+ ORDER BY session_date DESC
619
+ """)
620
+ dates = [row[0] for row in await cursor.fetchall()]
621
+
622
+ streak_days = 0
623
+ if dates:
624
+ current_date = datetime.now().date()
625
+ for i, date_str in enumerate(dates):
626
+ session_date = datetime.fromisoformat(date_str).date()
627
+ expected_date = current_date - timedelta(days=i)
628
+ if session_date == expected_date:
629
+ streak_days += 1
630
+ else:
631
+ break
632
+
633
+ return {
634
+ 'total_sessions': total_sessions,
635
+ 'total_focus_time': int(total_focus_time),
636
+ 'avg_focus_score': round(avg_focus_score, 3),
637
+ 'streak_days': streak_days
638
+ }
639
+
640
+ # ================ HEALTH CHECK ================
641
+
642
+ @app.get("/health")
643
+ async def health_check():
644
+ """Health check endpoint"""
645
+ return {
646
+ "status": "healthy",
647
+ "model_loaded": model is not None,
648
+ "database": os.path.exists(db_path)
649
+ }
focus_guard.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb042116505f0019d73e87a8262be3ef313b2b1f1471c3fd9bd1d61ee34d73ea
3
+ size 1105920