| """ |
| Nexus-Nano Inference API - Path Fixed |
| Model: /app/models/nexus-nano.onnx |
| Ultra-lightweight single-file engine |
| """ |
|
|
| from fastapi import FastAPI, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel, Field |
| import onnxruntime as ort |
| import numpy as np |
| import chess |
| import time |
| import logging |
| import os |
| from typing import Optional, Tuple |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(levelname)s - %(message)s' |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
|
|
| class NexusNanoEngine: |
| """Ultra-lightweight chess engine""" |
| |
| PIECE_VALUES = { |
| chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, |
| chess.ROOK: 5, chess.QUEEN: 9, chess.KING: 0 |
| } |
| |
| def __init__(self, model_path: str): |
| if not os.path.exists(model_path): |
| raise FileNotFoundError(f"Model not found: {model_path}") |
| |
| logger.info(f"π¦ Loading model: {model_path}") |
| logger.info(f"πΎ Size: {os.path.getsize(model_path)/(1024*1024):.2f} MB") |
| |
| sess_options = ort.SessionOptions() |
| sess_options.intra_op_num_threads = 2 |
| sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL |
| |
| self.session = ort.InferenceSession( |
| model_path, |
| sess_options=sess_options, |
| providers=['CPUExecutionProvider'] |
| ) |
| |
| self.input_name = self.session.get_inputs()[0].name |
| self.output_name = self.session.get_outputs()[0].name |
| self.nodes = 0 |
| |
| logger.info("β
Engine ready!") |
| |
| def fen_to_tensor(self, fen: str) -> np.ndarray: |
| board = chess.Board(fen) |
| tensor = np.zeros((1, 12, 8, 8), dtype=np.float32) |
| |
| piece_map = { |
| chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2, |
| chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5 |
| } |
| |
| for sq, piece in board.piece_map().items(): |
| r, f = divmod(sq, 8) |
| ch = piece_map[piece.piece_type] + (6 if piece.color == chess.BLACK else 0) |
| tensor[0, ch, r, f] = 1.0 |
| |
| return tensor |
| |
| def evaluate(self, board: chess.Board) -> float: |
| self.nodes += 1 |
| tensor = self.fen_to_tensor(board.fen()) |
| output = self.session.run([self.output_name], {self.input_name: tensor}) |
| score = float(output[0][0][0]) * 400.0 |
| return -score if board.turn == chess.BLACK else score |
| |
| def order_moves(self, board: chess.Board, moves): |
| scored = [] |
| for m in moves: |
| s = 0 |
| if board.is_capture(m): |
| v = board.piece_at(m.to_square) |
| a = board.piece_at(m.from_square) |
| if v and a: |
| s = self.PIECE_VALUES.get(v.piece_type, 0) * 10 |
| s -= self.PIECE_VALUES.get(a.piece_type, 0) |
| if m.promotion == chess.QUEEN: |
| s += 90 |
| scored.append((s, m)) |
| scored.sort(key=lambda x: x[0], reverse=True) |
| return [m for _, m in scored] |
| |
| def alpha_beta( |
| self, |
| board: chess.Board, |
| depth: int, |
| alpha: float, |
| beta: float |
| ) -> Tuple[float, Optional[chess.Move]]: |
| |
| if board.is_game_over(): |
| return (-10000 if board.is_checkmate() else 0), None |
| |
| if depth == 0: |
| return self.evaluate(board), None |
| |
| moves = list(board.legal_moves) |
| if not moves: |
| return 0, None |
| |
| moves = self.order_moves(board, moves) |
| |
| best_move = moves[0] |
| best_score = float('-inf') |
| |
| for move in moves: |
| board.push(move) |
| score, _ = self.alpha_beta(board, depth - 1, -beta, -alpha) |
| score = -score |
| board.pop() |
| |
| if score > best_score: |
| best_score = score |
| best_move = move |
| |
| alpha = max(alpha, score) |
| if alpha >= beta: |
| break |
| |
| return best_score, best_move |
| |
| def search(self, fen: str, depth: int = 3): |
| board = chess.Board(fen) |
| self.nodes = 0 |
| |
| moves = list(board.legal_moves) |
| if len(moves) == 0: |
| return {'best_move': '0000', 'evaluation': 0.0, 'nodes': 0, 'depth': 0} |
| |
| if len(moves) == 1: |
| return { |
| 'best_move': moves[0].uci(), |
| 'evaluation': round(self.evaluate(board) / 100.0, 2), |
| 'nodes': 1, |
| 'depth': 0 |
| } |
| |
| best_move = moves[0] |
| best_score = float('-inf') |
| current_depth = 1 |
| |
| for d in range(1, depth + 1): |
| try: |
| score, move = self.alpha_beta(board, d, float('-inf'), float('inf')) |
| if move: |
| best_move = move |
| best_score = score |
| current_depth = d |
| except: |
| break |
| |
| return { |
| 'best_move': best_move.uci(), |
| 'evaluation': round(best_score / 100.0, 2), |
| 'depth': current_depth, |
| 'nodes': self.nodes |
| } |
|
|
|
|
| |
|
|
| app = FastAPI( |
| title="Nexus-Nano Inference API", |
| description="Ultra-lightweight chess engine", |
| version="1.0.0" |
| ) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| engine = None |
|
|
|
|
| class MoveRequest(BaseModel): |
| fen: str |
| depth: Optional[int] = Field(3, ge=1, le=5) |
|
|
|
|
| class MoveResponse(BaseModel): |
| best_move: str |
| evaluation: float |
| depth_searched: int |
| nodes_evaluated: int |
| time_taken: int |
|
|
|
|
| @app.on_event("startup") |
| async def startup(): |
| global engine |
| logger.info("π Starting Nexus-Nano API...") |
| |
| |
| model_path = "/app/models/nexus-nano.onnx" |
| |
| logger.info(f"π Looking for: {model_path}") |
| |
| if os.path.exists("/app/models"): |
| logger.info("π Files in /app/models/:") |
| for f in os.listdir("/app/models"): |
| full_path = os.path.join("/app/models", f) |
| if os.path.isfile(full_path): |
| size = os.path.getsize(full_path) / (1024*1024) |
| logger.info(f" β {f} ({size:.2f} MB)") |
| else: |
| logger.error("β /app/models/ not found!") |
| raise FileNotFoundError("/app/models/ directory missing") |
| |
| if not os.path.exists(model_path): |
| logger.error(f"β Model not found: {model_path}") |
| logger.error("π‘ Available:", os.listdir("/app/models")) |
| raise FileNotFoundError(f"Missing: {model_path}") |
| |
| try: |
| engine = NexusNanoEngine(model_path) |
| logger.info("π Nexus-Nano ready!") |
| except Exception as e: |
| logger.error(f"β Load failed: {e}", exc_info=True) |
| raise |
|
|
|
|
| @app.get("/health") |
| async def health(): |
| return { |
| "status": "healthy" if engine else "unhealthy", |
| "model": "nexus-nano", |
| "version": "1.0.0", |
| "model_loaded": engine is not None, |
| "model_path": "/app/models/nexus-nano.onnx" |
| } |
|
|
|
|
| @app.post("/get-move", response_model=MoveResponse) |
| async def get_move(req: MoveRequest): |
| if not engine: |
| raise HTTPException(status_code=503, detail="Engine not loaded") |
| |
| try: |
| chess.Board(req.fen) |
| except: |
| raise HTTPException(status_code=400, detail="Invalid FEN") |
| |
| start = time.time() |
| |
| try: |
| result = engine.search(req.fen, req.depth) |
| elapsed = int((time.time() - start) * 1000) |
| |
| logger.info( |
| f"β Move: {result['best_move']} | " |
| f"Eval: {result['evaluation']:+.2f} | " |
| f"Depth: {result['depth']} | " |
| f"Nodes: {result['nodes']} | " |
| f"Time: {elapsed}ms" |
| ) |
| |
| return MoveResponse( |
| best_move=result['best_move'], |
| evaluation=result['evaluation'], |
| depth_searched=result['depth'], |
| nodes_evaluated=result['nodes'], |
| time_taken=elapsed |
| ) |
| |
| except Exception as e: |
| logger.error(f"β Search error: {e}", exc_info=True) |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @app.get("/") |
| async def root(): |
| return { |
| "name": "Nexus-Nano Inference API", |
| "version": "1.0.0", |
| "model": "2.8M parameters", |
| "architecture": "Compact ResNet", |
| "speed": "0.2-0.5s per move @ depth 3", |
| "status": "online" if engine else "starting", |
| "endpoints": { |
| "POST /get-move": "Get best move", |
| "GET /health": "Health check", |
| "GET /docs": "API docs" |
| } |
| } |
|
|
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run( |
| app, |
| host="0.0.0.0", |
| port=7860, |
| log_level="info", |
| access_log=True |
| ) |