Spaces:
Running
Running
| """Chess Vision API — board digitization + human-like move prediction. | |
| GET /health | |
| POST /digitize multipart image -> FEN (board placement) | |
| POST /predict-move { fen, top_n } -> CNN / Stockfish / hybrid moves | |
| GET /docs Swagger UI | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from pathlib import Path | |
| import cv2 as cv | |
| import numpy as np | |
| import onnxruntime as ort | |
| from fastapi import FastAPI, File, HTTPException, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| from .digitize import image_to_fen | |
| from .predict import MovePredictor | |
| from .sanitize import sanitize | |
| ROOT = Path(__file__).parent.parent | |
| MODELS_DIR = os.environ.get("MODELS_DIR", str(ROOT / "models_onnx")) | |
| STOCKFISH_PATH = os.environ.get("STOCKFISH_PATH", str(ROOT / "stockfish" / "stockfish")) | |
| app = FastAPI( | |
| title="Chess Vision API", | |
| description="Digitize a chess board photo to a FEN, then predict human-like moves " | |
| "(CNN trained on Lichess games) combined with Stockfish.", | |
| version="0.1.0", | |
| ) | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) | |
| # loaded once at startup | |
| _digitizer = ort.InferenceSession(str(Path(MODELS_DIR) / "digitizer3d.fp32.onnx"), | |
| providers=["CPUExecutionProvider"]) | |
| _predictor = MovePredictor(MODELS_DIR, stockfish_path=STOCKFISH_PATH) | |
| class PredictRequest(BaseModel): | |
| fen: str = Field(..., description="FEN (full or placement-only).", | |
| examples=["rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1"]) | |
| top_n: int = Field(3, ge=1, le=8) | |
| def root(): | |
| return {"service": "Chess Vision API", "version": app.version, | |
| "stockfish": _predictor.stockfish_path is not None, "docs": "/docs"} | |
| def health(): | |
| return {"status": "ok", "stockfish": _predictor.stockfish_path is not None} | |
| async def digitize(file: UploadFile = File(...)): | |
| """Upload a board image (render-style) and get its FEN board placement.""" | |
| data = await file.read() | |
| img = cv.imdecode(np.frombuffer(data, np.uint8), cv.IMREAD_COLOR) | |
| if img is None: | |
| raise HTTPException(400, "Could not decode the image.") | |
| placement = image_to_fen(img, _digitizer) | |
| if placement is None: | |
| raise HTTPException(422, "Could not detect a full 8x8 board in the image.") | |
| fixed = sanitize(placement) # repair impossible positions (back-rank pawns, king count, …) | |
| return { | |
| "raw_placement": placement, | |
| "placement": fixed["placement"], | |
| "fen": fixed["fen"], | |
| "corrections": fixed["corrections"], | |
| "valid": fixed["valid"], | |
| } | |
| def predict_move(req: PredictRequest): | |
| try: | |
| return _predictor.predict(req.fen, top_n=req.top_n) | |
| except ValueError as e: | |
| raise HTTPException(400, f"Invalid FEN: {e}") | |