captcha-solver-api / scripts /run_learning_cycle.py
ballagb19's picture
Upload scripts/run_learning_cycle.py with huggingface_hub
b3eaf23 verified
Raw
History Blame Contribute Delete
4.84 kB
"""Hourly learning cycle: test solvers, record results, analyze, optimize.
Run locally: python scripts/run_learning_cycle.py
Run via GH Action: triggered by .github/workflows/learn.yml
This simulates captcha solves across all solver types to gather
accuracy data over time. The learning DB is stored at data/learning.db
and exported as a GitHub Actions artifact.
"""
from __future__ import annotations
import json
import os
import random
import sys
import time
from pathlib import Path
# Add parent to path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from captcha_solver.learning.db import LearningDB
from captcha_solver.learning.trainer import Trainer
def generate_math_samples(count: int = 20) -> list[dict]:
"""Generate math captcha test cases: one per method (regex, llm, tesseract)."""
samples = []
ops = [
(lambda a, b: a + b, "+"),
(lambda a, b: a - b, "-"),
(lambda a, b: a * b, "*"),
]
for _ in range(count):
a = random.randint(1, 99)
b = random.randint(1, min(99, a if False else 99))
op_fn, op_str = random.choice(ops)
if op_str == "-" and b > a:
a, b = b, a
result = op_fn(a, b)
samples.append({
"type": "math",
"hint": f"{a} {op_str} {b} = ?",
"expected": str(result),
})
return samples
def simulate_solve(sample: dict, solver: str) -> dict:
"""Simulate a solver attempt (no real models needed for CI)."""
start = time.time()
# Simulate latency
latency = random.uniform(0.05, 0.5)
time.sleep(latency * 0.01) # 1% of real time for speed
# Simulated accuracy per solver
accuracy_map = {
"regex": 0.92,
"llm": 0.88,
"tesseract": 0.65,
"florence2": 0.75,
"moondream2": 0.50,
"qwen": 0.70,
"whisper": 0.80,
}
accuracy = accuracy_map.get(solver, 0.5)
is_correct = random.random() < accuracy
answer = sample["expected"] if is_correct else str(random.randint(0, 999))
elapsed = int((time.time() - start) * 1000)
return {
"answer": answer,
"expected": sample["expected"],
"correct": is_correct,
"latency_ms": elapsed,
"confidence": accuracy if is_correct else accuracy * 0.3,
}
def run_cycle(output_dir: str | Path | None = None) -> dict:
"""Run one full learning cycle.
Args:
output_dir: if set, saves results as JSON here (for GH artifact).
"""
db = LearningDB()
trainer = Trainer(db)
# Start cycle
cycle_id = db.start_cycle()
print(f"[cycle {cycle_id}] starting...")
solvers = ["regex", "llm", "tesseract", "florence2", "moondream2", "qwen", "whisper"]
samples = generate_math_samples(30)
total = 0
correct = 0
latencies = []
for sample in samples:
solver = random.choice(solvers)
result = simulate_solve(sample, solver)
db.record_attempt(
captcha_type=sample["type"],
solver_used=solver,
answer=result["answer"],
expected=result["expected"],
correct=result["correct"],
confidence=result.get("confidence"),
latency_ms=result["latency_ms"],
hint=sample.get("hint"),
run_source="learning_cycle",
)
total += 1
if result["correct"]:
correct += 1
latencies.append(result["latency_ms"])
status = "OK" if result["correct"] else "FAIL"
print(f" [{solver:12}] {sample['hint']:20} -> {result['answer']:6} ({status})")
avg_lat = sum(latencies) / max(len(latencies), 1)
db.finish_cycle(cycle_id, total, correct, avg_lat)
# Analyze
analysis = trainer.optimize(cycle_id)
print(f"\n[cycle {cycle_id}] complete: {correct}/{total} correct ({correct/max(total,1)*100:.1f}%), avg {avg_lat:.0f}ms")
result = {
"cycle_id": cycle_id,
"total": total,
"correct": correct,
"accuracy_pct": round(correct / max(total, 1) * 100, 1),
"avg_latency_ms": round(avg_lat),
"changes": analysis,
"stats": db.summary(),
"recommendations": analysis,
}
if output_dir:
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
(out / "cycle_result.json").write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
print(f"\nresult saved to {out / 'cycle_result.json'}")
return result
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--output", "-o", default=None, help="Output directory for results JSON")
args = parser.parse_args()
result = run_cycle(args.output)
print(f"\naccuracy: {result['accuracy_pct']}% over {result['total']} solves")