Spaces:
Sleeping
Sleeping
File size: 8,217 Bytes
07b66c8 | 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 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """SQLite learning database for tracking solver accuracy and optimization history."""
from __future__ import annotations
import json
import sqlite3
import time
from pathlib import Path
from typing import Any
DATA_DIR = Path(__file__).resolve().parent.parent.parent / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_PATH = DATA_DIR / "learning.db"
SCHEMA = """
CREATE TABLE IF NOT EXISTS attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
captcha_type TEXT NOT NULL,
solver_used TEXT NOT NULL,
hint TEXT,
image_hash TEXT,
answer TEXT,
expected TEXT,
correct INTEGER,
confidence REAL,
latency_ms INTEGER,
preprocess_steps TEXT,
run_source TEXT DEFAULT 'api',
timestamp TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS solver_stats (
solver_name TEXT NOT NULL,
captcha_type TEXT NOT NULL,
total INTEGER DEFAULT 0,
correct INTEGER DEFAULT 0,
avg_latency_ms REAL DEFAULT 0,
last_run TEXT DEFAULT (datetime('now')),
PRIMARY KEY (solver_name, captcha_type)
);
CREATE TABLE IF NOT EXISTS optimization_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cycle INTEGER,
action TEXT,
before_val TEXT,
after_val TEXT,
before_acc REAL,
after_acc REAL,
notes TEXT,
timestamp TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS cycles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT DEFAULT 'running',
total_solves INTEGER DEFAULT 0,
correct_solves INTEGER DEFAULT 0,
avg_latency_ms REAL DEFAULT 0,
started_at TEXT DEFAULT (datetime('now')),
finished_at TEXT
);
"""
class LearningDB:
def __init__(self, db_path: str | Path = DB_PATH) -> None:
self.db_path = Path(db_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._conn: sqlite3.Connection | None = None
@property
def conn(self) -> sqlite3.Connection:
if self._conn is None:
self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
self._conn.row_factory = sqlite3.Row
self._conn.executescript(SCHEMA)
self._conn.commit()
return self._conn
def close(self) -> None:
if self._conn:
self._conn.close()
self._conn = None
# --- Attempts ---
def record_attempt(
self,
captcha_type: str,
solver_used: str,
answer: str | None,
expected: str | None = None,
correct: bool | None = None,
confidence: float | None = None,
latency_ms: int | None = None,
hint: str | None = None,
image_hash: str | None = None,
preprocess_steps: str | None = None,
run_source: str = "api",
) -> int:
if correct is None and expected is not None and answer is not None:
correct = answer.strip().lower() == expected.strip().lower()
cur = self.conn.execute(
"""INSERT INTO attempts
(captcha_type, solver_used, hint, image_hash, answer, expected, correct, confidence, latency_ms, preprocess_steps, run_source)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(captcha_type, solver_used, hint, image_hash, answer, expected,
int(correct) if correct is not None else None,
confidence, latency_ms, preprocess_steps, run_source),
)
self.conn.commit()
self._update_stats(solver_used, captcha_type, correct, latency_ms)
return cur.lastrowid
def _update_stats(self, solver: str, ctype: str, correct: bool | None, latency_ms: int | None) -> None:
row = self.conn.execute(
"SELECT total, correct, avg_latency_ms FROM solver_stats WHERE solver_name=? AND captcha_type=?",
(solver, ctype)
).fetchone()
if row:
t = row["total"] + 1
c = row["correct"] + (1 if correct else 0) if correct is not None else row["correct"]
avg = row["avg_latency_ms"]
if latency_ms:
avg = ((avg * row["total"]) + latency_ms) / t
self.conn.execute(
"UPDATE solver_stats SET total=?, correct=?, avg_latency_ms=?, last_run=datetime('now') WHERE solver_name=? AND captcha_type=?",
(t, c, avg, solver, ctype)
)
else:
self.conn.execute(
"INSERT INTO solver_stats (solver_name, captcha_type, total, correct, avg_latency_ms) VALUES (?,?,1,?,?)",
(solver, ctype, (1 if correct else 0) if correct is not None else 0, latency_ms or 0)
)
self.conn.commit()
# --- Stats ---
def get_best_solver(self, captcha_type: str, min_samples: int = 5) -> dict | None:
rows = self.conn.execute(
"""SELECT solver_name, total, correct,
CAST(correct AS REAL) / MAX(total, 1) AS accuracy,
avg_latency_ms
FROM solver_stats
WHERE captcha_type=? AND total>=?
ORDER BY accuracy DESC, total DESC
LIMIT 1""",
(captcha_type, min_samples),
).fetchall()
if rows:
d = dict(rows[0])
d["accuracy"] = round(d["accuracy"], 3) if d.get("accuracy") is not None else 0.0
return d
return None
def get_solver_ranking(self, captcha_type: str | None = None) -> list[dict]:
if captcha_type:
rows = self.conn.execute(
"SELECT * FROM solver_stats WHERE captcha_type=? ORDER BY CAST(correct AS REAL)/MAX(total,1) DESC",
(captcha_type,)
).fetchall()
else:
rows = self.conn.execute(
"SELECT * FROM solver_stats ORDER BY CAST(correct AS REAL)/MAX(total,1) DESC"
).fetchall()
return [dict(r) for r in rows]
def get_recent_failures(self, limit: int = 20) -> list[dict]:
rows = self.conn.execute(
"""SELECT * FROM attempts WHERE correct=0 AND answer IS NOT NULL
ORDER BY timestamp DESC LIMIT ?""",
(limit,)
).fetchall()
return [dict(r) for r in rows]
# --- Cycles ---
def start_cycle(self) -> int:
cur = self.conn.execute("INSERT INTO cycles (status) VALUES ('running')")
self.conn.commit()
return cur.lastrowid
def finish_cycle(self, cycle_id: int, total: int, correct: int, avg_latency: float) -> None:
self.conn.execute(
"UPDATE cycles SET status='completed', total_solves=?, correct_solves=?, avg_latency_ms=?, finished_at=datetime('now') WHERE id=?",
(total, correct, avg_latency, cycle_id)
)
self.conn.commit()
def log_optimization(
self, cycle: int, action: str,
before_val: str, after_val: str,
before_acc: float, after_acc: float,
notes: str = ""
) -> int:
cur = self.conn.execute(
"INSERT INTO optimization_log (cycle, action, before_val, after_val, before_acc, after_acc, notes) VALUES (?,?,?,?,?,?,?)",
(cycle, action, before_val, after_val, before_acc, after_acc, notes)
)
self.conn.commit()
return cur.lastrowid
# --- Export ---
def summary(self) -> dict:
totals = self.conn.execute(
"SELECT COUNT(*) as total, SUM(CASE WHEN correct=1 THEN 1 ELSE 0 END) as correct, AVG(latency_ms) as avg_ms FROM attempts"
).fetchone()
acc = round((totals["correct"] / max(totals["total"], 1)) * 100, 1) if totals["total"] else 0
return {
"total_attempts": totals["total"],
"correct": totals["correct"],
"accuracy_pct": acc,
"avg_latency_ms": round(totals["avg_ms"] or 0),
"solver_count": self.conn.execute("SELECT COUNT(*) FROM solver_stats").fetchone()[0],
"optimization_cycles": self.conn.execute("SELECT COUNT(*) FROM optimization_log").fetchone()[0],
}
def get_recent_attempts(self, limit: int = 50) -> list[dict]:
rows = self.conn.execute("SELECT * FROM attempts ORDER BY timestamp DESC LIMIT ?", (limit,)).fetchall()
return [dict(r) for r in rows]
|