set-solver / src /web /app.py
Tian Wang
Add 70% detection and 80% classifier thresholds
91a905e
"""
Web-based real-time Set solver.
FastAPI backend serving a single HTML page with live camera feed.
Processes frames via the SetSolver pipeline and returns annotated results.
"""
import base64
import io
import sys
from pathlib import Path
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import HTMLResponse
from PIL import Image
# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.inference.solve import SetSolver
app = FastAPI(title="Set Solver")
# Global solver instance (loaded once at startup)
solver: SetSolver = None
@app.on_event("startup")
def load_solver():
global solver
print("Loading Set Solver pipeline...")
solver = SetSolver()
print("Solver ready!")
@app.get("/", response_class=HTMLResponse)
def index():
html_path = Path(__file__).parent / "templates" / "index.html"
return html_path.read_text()
@app.post("/api/solve")
async def solve_frame(file: UploadFile = File(...)):
"""Accept a JPEG frame, run solver, return results."""
contents = await file.read()
image = Image.open(io.BytesIO(contents)).convert("RGB")
result = solver.solve_from_image(image, conf=0.7, cls_conf=0.8)
# Encode per-set annotated images as base64 JPEG
result_images_b64 = []
for img in result.pop("result_images"):
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=85)
result_images_b64.append(base64.b64encode(buf.getvalue()).decode("utf-8"))
result["result_images_b64"] = result_images_b64
# Crop cards per set for trophy display
per_set_cards_b64 = []
for bboxes in result.get("sets_bboxes", []):
crops = []
for bbox in bboxes:
x1, y1, x2, y2 = bbox
crop = image.crop((x1, y1, x2, y2))
cbuf = io.BytesIO()
crop.save(cbuf, format="JPEG", quality=90)
crops.append(base64.b64encode(cbuf.getvalue()).decode("utf-8"))
per_set_cards_b64.append(crops)
result["per_set_cards_b64"] = per_set_cards_b64
return result
if __name__ == "__main__":
import argparse
import subprocess
import tempfile
import uvicorn
parser = argparse.ArgumentParser(description="Set Solver web server")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--no-ssl", action="store_true", help="Disable auto-generated SSL (camera requires HTTPS on non-localhost)")
args = parser.parse_args()
ssl_kwargs = {}
if not args.no_ssl:
# Generate a self-signed cert so mobile browsers allow camera access
cert_dir = Path(tempfile.mkdtemp())
cert_file = cert_dir / "cert.pem"
key_file = cert_dir / "key.pem"
subprocess.run([
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", str(key_file), "-out", str(cert_file),
"-days", "1", "-nodes",
"-subj", "/CN=set-solver",
], check=True, capture_output=True)
ssl_kwargs = {"ssl_certfile": str(cert_file), "ssl_keyfile": str(key_file)}
proto = "https"
else:
proto = "http"
# Show access URLs
import socket
hostname = socket.gethostname()
try:
local_ip = socket.gethostbyname(hostname)
except socket.gaierror:
local_ip = "127.0.0.1"
print(f"\n Set Solver running at:")
print(f" Local: {proto}://localhost:{args.port}")
print(f" Network: {proto}://{local_ip}:{args.port}\n")
uvicorn.run("src.web.app:app", host="0.0.0.0", port=args.port, reload=False, **ssl_kwargs)