Spaces:
Sleeping
Sleeping
| """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") | |