File size: 7,091 Bytes
eb4abb8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"""SQLite DB for focus sessions and user settings."""

from __future__ import annotations

import asyncio
import json
from datetime import datetime

import aiosqlite


def get_db_path() -> str:
    """Database file path from config or default."""
    try:
        from config import get
        return get("app.db_path") or "focus_guard.db"
    except Exception:
        return "focus_guard.db"


async def init_database(db_path: str | None = None) -> None:
    """Create focus_sessions, focus_events, user_settings tables if missing."""
    path = db_path or get_db_path()
    async with aiosqlite.connect(path) as db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS focus_sessions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                start_time TIMESTAMP NOT NULL,
                end_time TIMESTAMP,
                duration_seconds INTEGER DEFAULT 0,
                focus_score REAL DEFAULT 0.0,
                total_frames INTEGER DEFAULT 0,
                focused_frames INTEGER DEFAULT 0,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        await db.execute("""
            CREATE TABLE IF NOT EXISTS focus_events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                session_id INTEGER NOT NULL,
                timestamp TIMESTAMP NOT NULL,
                is_focused BOOLEAN NOT NULL,
                confidence REAL NOT NULL,
                detection_data TEXT,
                FOREIGN KEY (session_id) REFERENCES focus_sessions (id)
            )
        """)
        await db.execute("""
            CREATE TABLE IF NOT EXISTS user_settings (
                id INTEGER PRIMARY KEY CHECK (id = 1),
                model_name TEXT DEFAULT 'mlp'
            )
        """)
        await db.execute("""
            INSERT OR IGNORE INTO user_settings (id, model_name)
            VALUES (1, 'mlp')
        """)
        await db.commit()


async def create_session(db_path: str | None = None) -> int:
    """Insert a new focus session. Returns session id."""
    path = db_path or get_db_path()
    async with aiosqlite.connect(path) as db:
        cursor = await db.execute(
            "INSERT INTO focus_sessions (start_time) VALUES (?)",
            (datetime.now().isoformat(),),
        )
        await db.commit()
        return cursor.lastrowid


async def end_session(session_id: int, db_path: str | None = None) -> dict | None:
    """Close session and return summary (duration, focus_score, etc.)."""
    path = db_path or get_db_path()
    async with aiosqlite.connect(path) as db:
        cursor = await db.execute(
            "SELECT start_time, total_frames, focused_frames FROM focus_sessions WHERE id = ?",
            (session_id,),
        )
        row = await cursor.fetchone()
    if not row:
        return None
    start_time_str, total_frames, focused_frames = row
    start_time = datetime.fromisoformat(start_time_str)
    end_time = datetime.now()
    duration = (end_time - start_time).total_seconds()
    focus_score = focused_frames / total_frames if total_frames > 0 else 0.0
    async with aiosqlite.connect(path) as db:
        await db.execute("""
            UPDATE focus_sessions
            SET end_time = ?, duration_seconds = ?, focus_score = ?
            WHERE id = ?
        """, (end_time.isoformat(), int(duration), focus_score, session_id))
        await db.commit()
    return {
        "session_id": session_id,
        "start_time": start_time_str,
        "end_time": end_time.isoformat(),
        "duration_seconds": int(duration),
        "focus_score": round(focus_score, 3),
        "total_frames": total_frames,
        "focused_frames": focused_frames,
    }


async def store_focus_event(
    session_id: int,
    is_focused: bool,
    confidence: float,
    metadata: dict,
    db_path: str | None = None,
) -> None:
    """Append one focus event and update session counters."""
    path = db_path or get_db_path()
    async with aiosqlite.connect(path) as db:
        await db.execute("""
            INSERT INTO focus_events (session_id, timestamp, is_focused, confidence, detection_data)
            VALUES (?, ?, ?, ?, ?)
        """, (session_id, datetime.now().isoformat(), is_focused, confidence, json.dumps(metadata)))
        await db.execute("""
            UPDATE focus_sessions
            SET total_frames = total_frames + 1,
                focused_frames = focused_frames + ?
            WHERE id = ?
        """, (1 if is_focused else 0, session_id))
        await db.commit()


class EventBuffer:
    """Buffer focus events and flush to DB in batches to avoid per-frame writes."""

    def __init__(self, db_path: str | None = None, flush_interval: float = 2.0):
        self._db_path = db_path or get_db_path()
        self._flush_interval = flush_interval
        self._buf: list = []
        self._lock = asyncio.Lock()
        self._task: asyncio.Task | None = None
        self._total_frames = 0
        self._focused_frames = 0

    def start(self) -> None:
        if self._task is None:
            self._task = asyncio.create_task(self._flush_loop())

    async def stop(self) -> None:
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
            self._task = None
        await self._flush()

    def add(self, session_id: int, is_focused: bool, confidence: float, metadata: dict) -> None:
        self._buf.append((
            session_id,
            datetime.now().isoformat(),
            is_focused,
            confidence,
            json.dumps(metadata),
        ))
        self._total_frames += 1
        if is_focused:
            self._focused_frames += 1

    async def _flush_loop(self) -> None:
        while True:
            await asyncio.sleep(self._flush_interval)
            await self._flush()

    async def _flush(self) -> None:
        async with self._lock:
            if not self._buf:
                return
            batch = self._buf[:]
            total = self._total_frames
            focused = self._focused_frames
            self._buf.clear()
            self._total_frames = 0
            self._focused_frames = 0
        if not batch:
            return
        session_id = batch[0][0]
        try:
            async with aiosqlite.connect(self._db_path) as db:
                await db.executemany("""
                    INSERT INTO focus_events (session_id, timestamp, is_focused, confidence, detection_data)
                    VALUES (?, ?, ?, ?, ?)
                """, batch)
                await db.execute("""
                    UPDATE focus_sessions
                    SET total_frames = total_frames + ?,
                        focused_frames = focused_frames + ?
                    WHERE id = ?
                """, (total, focused, session_id))
                await db.commit()
        except Exception as e:
            import logging
            logging.getLogger(__name__).warning("DB flush error: %s", e)