ballagb19 commited on
Commit
9a6dbb3
·
verified ·
1 Parent(s): 32800ec

Upload captcha_solver/utils/cache.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. captcha_solver/utils/cache.py +79 -0
captcha_solver/utils/cache.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LRU+TTL cache for captcha answers, keyed by perceptual hash."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass
8
+ from threading import Lock
9
+ from typing import Optional
10
+
11
+
12
+ @dataclass
13
+ class _Entry:
14
+ answer: str
15
+ solver: str
16
+ confidence: float
17
+ expires_at: float
18
+ hits: int = 0
19
+
20
+
21
+ class SolveCache:
22
+ """Thread-safe in-memory LRU cache with TTL.
23
+
24
+ Keys are perceptual hashes (hex strings). Values are the solved answer
25
+ plus the solver that produced it. Entries expire after `ttl_seconds`
26
+ and are evicted LRU when over capacity.
27
+ """
28
+
29
+ def __init__(self, ttl_seconds: int = 3600, max_entries: int = 10_000):
30
+ self.ttl = ttl_seconds
31
+ self.max = max_entries
32
+ self._store: OrderedDict[str, _Entry] = OrderedDict()
33
+ self._lock = Lock()
34
+ self.hits = 0
35
+ self.misses = 0
36
+
37
+ def get(self, key: str) -> Optional[_Entry]:
38
+ now = time.time()
39
+ with self._lock:
40
+ entry = self._store.get(key)
41
+ if entry is None:
42
+ self.misses += 1
43
+ return None
44
+ if entry.expires_at < now:
45
+ del self._store[key]
46
+ self.misses += 1
47
+ return None
48
+ entry.hits += 1
49
+ self.hits += 1
50
+ self._store.move_to_end(key)
51
+ return entry
52
+
53
+ def set(self, key: str, answer: str, solver: str, confidence: float) -> None:
54
+ now = time.time()
55
+ with self._lock:
56
+ self._store[key] = _Entry(
57
+ answer=answer,
58
+ solver=solver,
59
+ confidence=confidence,
60
+ expires_at=now + self.ttl,
61
+ )
62
+ self._store.move_to_end(key)
63
+ while len(self._store) > self.max:
64
+ self._store.popitem(last=False)
65
+
66
+ def stats(self) -> dict:
67
+ with self._lock:
68
+ total = self.hits + self.misses
69
+ return {
70
+ "size": len(self._store),
71
+ "max": self.max,
72
+ "hits": self.hits,
73
+ "misses": self.misses,
74
+ "hit_rate": (self.hits / total) if total else 0.0,
75
+ }
76
+
77
+ def clear(self) -> None:
78
+ with self._lock:
79
+ self._store.clear()