Rafs-an09002 commited on
Commit
c10889a
Β·
verified Β·
1 Parent(s): 9376e30

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -189
app.py CHANGED
@@ -1,9 +1,3 @@
1
- """
2
- Nexus-Nano Inference API - Path Fixed
3
- Model: /app/models/nexus-nano.onnx
4
- Ultra-lightweight single-file engine
5
- """
6
-
7
  from fastapi import FastAPI, HTTPException
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from pydantic import BaseModel, Field
@@ -15,59 +9,36 @@ import logging
15
  import os
16
  from typing import Optional, Tuple
17
 
18
- logging.basicConfig(
19
- level=logging.INFO,
20
- format='%(asctime)s - %(levelname)s - %(message)s'
21
- )
22
  logger = logging.getLogger(__name__)
23
 
24
- # ==================== NANO ENGINE ====================
25
-
26
  class NexusNanoEngine:
27
- """Ultra-lightweight chess engine"""
28
-
29
- PIECE_VALUES = {
30
- chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3,
31
- chess.ROOK: 5, chess.QUEEN: 9, chess.KING: 0
32
- }
33
 
34
  def __init__(self, model_path: str):
35
  if not os.path.exists(model_path):
36
  raise FileNotFoundError(f"Model not found: {model_path}")
37
 
38
- logger.info(f"πŸ“¦ Loading model: {model_path}")
39
- logger.info(f"πŸ’Ύ Size: {os.path.getsize(model_path)/(1024*1024):.2f} MB")
40
 
41
  sess_options = ort.SessionOptions()
42
  sess_options.intra_op_num_threads = 2
43
  sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
44
 
45
- self.session = ort.InferenceSession(
46
- model_path,
47
- sess_options=sess_options,
48
- providers=['CPUExecutionProvider']
49
- )
50
-
51
  self.input_name = self.session.get_inputs()[0].name
52
  self.output_name = self.session.get_outputs()[0].name
53
  self.nodes = 0
54
-
55
- logger.info("βœ… Engine ready!")
56
 
57
  def fen_to_tensor(self, fen: str) -> np.ndarray:
58
  board = chess.Board(fen)
59
  tensor = np.zeros((1, 12, 8, 8), dtype=np.float32)
60
-
61
- piece_map = {
62
- chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2,
63
- chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5
64
- }
65
-
66
  for sq, piece in board.piece_map().items():
67
  r, f = divmod(sq, 8)
68
  ch = piece_map[piece.piece_type] + (6 if piece.color == chess.BLACK else 0)
69
  tensor[0, ch, r, f] = 1.0
70
-
71
  return tensor
72
 
73
  def evaluate(self, board: chess.Board) -> float:
@@ -77,123 +48,65 @@ class NexusNanoEngine:
77
  score = float(output[0][0][0]) * 400.0
78
  return -score if board.turn == chess.BLACK else score
79
 
80
- def order_moves(self, board: chess.Board, moves):
81
  scored = []
82
  for m in moves:
83
  s = 0
84
  if board.is_capture(m):
85
- v = board.piece_at(m.to_square)
86
- a = board.piece_at(m.from_square)
87
  if v and a:
88
- s = self.PIECE_VALUES.get(v.piece_type, 0) * 10
89
- s -= self.PIECE_VALUES.get(a.piece_type, 0)
90
- if m.promotion == chess.QUEEN:
91
- s += 90
92
  scored.append((s, m))
93
  scored.sort(key=lambda x: x[0], reverse=True)
94
  return [m for _, m in scored]
95
 
96
- def alpha_beta(
97
- self,
98
- board: chess.Board,
99
- depth: int,
100
- alpha: float,
101
- beta: float
102
- ) -> Tuple[float, Optional[chess.Move]]:
103
-
104
  if board.is_game_over():
105
  return (-10000 if board.is_checkmate() else 0), None
106
-
107
  if depth == 0:
108
  return self.evaluate(board), None
109
-
110
  moves = list(board.legal_moves)
111
- if not moves:
112
- return 0, None
113
-
114
  moves = self.order_moves(board, moves)
115
-
116
- best_move = moves[0]
117
- best_score = float('-inf')
118
-
119
  for move in moves:
120
  board.push(move)
121
  score, _ = self.alpha_beta(board, depth - 1, -beta, -alpha)
122
  score = -score
123
  board.pop()
124
-
125
  if score > best_score:
126
- best_score = score
127
- best_move = move
128
-
129
  alpha = max(alpha, score)
130
- if alpha >= beta:
131
- break
132
-
133
  return best_score, best_move
134
 
135
  def search(self, fen: str, depth: int = 3):
136
  board = chess.Board(fen)
137
  self.nodes = 0
138
-
139
  moves = list(board.legal_moves)
140
  if len(moves) == 0:
141
  return {'best_move': '0000', 'evaluation': 0.0, 'nodes': 0, 'depth': 0}
142
-
143
  if len(moves) == 1:
144
- return {
145
- 'best_move': moves[0].uci(),
146
- 'evaluation': round(self.evaluate(board) / 100.0, 2),
147
- 'nodes': 1,
148
- 'depth': 0
149
- }
150
-
151
- best_move = moves[0]
152
- best_score = float('-inf')
153
- current_depth = 1
154
-
155
  for d in range(1, depth + 1):
156
  try:
157
  score, move = self.alpha_beta(board, d, float('-inf'), float('inf'))
158
  if move:
159
- best_move = move
160
- best_score = score
161
- current_depth = d
162
- except:
163
- break
164
-
165
- return {
166
- 'best_move': best_move.uci(),
167
- 'evaluation': round(best_score / 100.0, 2),
168
- 'depth': current_depth,
169
- 'nodes': self.nodes
170
- }
171
-
172
-
173
- # ==================== FASTAPI APP ====================
174
 
175
- app = FastAPI(
176
- title="Nexus-Nano Inference API",
177
- description="Ultra-lightweight chess engine",
178
- version="1.0.0"
179
- )
180
-
181
- app.add_middleware(
182
- CORSMiddleware,
183
- allow_origins=["*"],
184
- allow_credentials=True,
185
- allow_methods=["*"],
186
- allow_headers=["*"],
187
- )
188
 
189
  engine = None
190
 
191
-
192
  class MoveRequest(BaseModel):
193
  fen: str
194
  depth: Optional[int] = Field(3, ge=1, le=5)
195
 
196
-
197
  class MoveResponse(BaseModel):
198
  best_move: str
199
  evaluation: float
@@ -201,112 +114,64 @@ class MoveResponse(BaseModel):
201
  nodes_evaluated: int
202
  time_taken: int
203
 
204
-
205
  @app.on_event("startup")
206
  async def startup():
207
  global engine
208
- logger.info("πŸš€ Starting Nexus-Nano API...")
209
-
210
- # FIXED: Correct path with hyphen
211
- model_path = "/app/models/nexus-nano.onnx"
212
 
213
- logger.info(f"πŸ” Looking for: {model_path}")
 
 
 
 
214
 
215
- if os.path.exists("app/models"):
216
- logger.info("πŸ“‚ Files in /app/models/:")
217
- for f in os.listdir("/app/models"):
218
- full_path = os.path.join("/app/models", f)
219
- if os.path.isfile(full_path):
220
- size = os.path.getsize(full_path) / (1024*1024)
221
- logger.info(f" βœ“ {f} ({size:.2f} MB)")
222
- else:
223
- logger.error("❌ /app/models/ not found!")
224
- raise FileNotFoundError("/app/models/ directory missing")
225
 
226
- if not os.path.exists(model_path):
227
- logger.error(f"❌ Model not found: {model_path}")
228
- logger.error("πŸ’‘ Available:", os.listdir("/app/models"))
229
- raise FileNotFoundError(f"Missing: {model_path}")
 
 
 
 
 
230
 
231
  try:
232
  engine = NexusNanoEngine(model_path)
233
- logger.info("πŸŽ‰ Nexus-Nano ready!")
234
  except Exception as e:
235
  logger.error(f"❌ Load failed: {e}", exc_info=True)
236
  raise
237
 
238
-
239
  @app.get("/health")
240
  async def health():
241
- return {
242
- "status": "healthy" if engine else "unhealthy",
243
- "model": "nexus-nano",
244
- "version": "1.0.0",
245
- "model_loaded": engine is not None,
246
- "model_path": "/app/models/nexus-nano.onnx"
247
- }
248
-
249
 
250
  @app.post("/get-move", response_model=MoveResponse)
251
  async def get_move(req: MoveRequest):
252
- if not engine:
253
- raise HTTPException(status_code=503, detail="Engine not loaded")
254
-
255
- try:
256
- chess.Board(req.fen)
257
- except:
258
- raise HTTPException(status_code=400, detail="Invalid FEN")
259
-
260
  start = time.time()
261
-
262
  try:
263
  result = engine.search(req.fen, req.depth)
264
  elapsed = int((time.time() - start) * 1000)
265
-
266
- logger.info(
267
- f"βœ“ Move: {result['best_move']} | "
268
- f"Eval: {result['evaluation']:+.2f} | "
269
- f"Depth: {result['depth']} | "
270
- f"Nodes: {result['nodes']} | "
271
- f"Time: {elapsed}ms"
272
- )
273
-
274
- return MoveResponse(
275
- best_move=result['best_move'],
276
- evaluation=result['evaluation'],
277
- depth_searched=result['depth'],
278
- nodes_evaluated=result['nodes'],
279
- time_taken=elapsed
280
- )
281
-
282
  except Exception as e:
283
- logger.error(f"❌ Search error: {e}", exc_info=True)
284
- raise HTTPException(status_code=500, detail=str(e))
285
-
286
 
287
  @app.get("/")
288
  async def root():
289
- return {
290
- "name": "Nexus-Nano Inference API",
291
- "version": "1.0.0",
292
- "model": "2.8M parameters",
293
- "architecture": "Compact ResNet",
294
- "speed": "0.2-0.5s per move @ depth 3",
295
- "status": "online" if engine else "starting",
296
- "endpoints": {
297
- "POST /get-move": "Get best move",
298
- "GET /health": "Health check",
299
- "GET /docs": "API docs"
300
- }
301
- }
302
-
303
 
304
  if __name__ == "__main__":
305
  import uvicorn
306
- uvicorn.run(
307
- app,
308
- host="0.0.0.0",
309
- port=7860,
310
- log_level="info",
311
- access_log=True
312
- )
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel, Field
 
9
  import os
10
  from typing import Optional, Tuple
11
 
12
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
13
  logger = logging.getLogger(__name__)
14
 
 
 
15
  class NexusNanoEngine:
16
+ PIECE_VALUES = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, chess.QUEEN: 9, chess.KING: 0}
 
 
 
 
 
17
 
18
  def __init__(self, model_path: str):
19
  if not os.path.exists(model_path):
20
  raise FileNotFoundError(f"Model not found: {model_path}")
21
 
22
+ logger.info(f"Loading: {model_path} ({os.path.getsize(model_path)/(1024*1024):.2f} MB)")
 
23
 
24
  sess_options = ort.SessionOptions()
25
  sess_options.intra_op_num_threads = 2
26
  sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
27
 
28
+ self.session = ort.InferenceSession(model_path, sess_options=sess_options, providers=['CPUExecutionProvider'])
 
 
 
 
 
29
  self.input_name = self.session.get_inputs()[0].name
30
  self.output_name = self.session.get_outputs()[0].name
31
  self.nodes = 0
32
+ logger.info("βœ… Engine ready")
 
33
 
34
  def fen_to_tensor(self, fen: str) -> np.ndarray:
35
  board = chess.Board(fen)
36
  tensor = np.zeros((1, 12, 8, 8), dtype=np.float32)
37
+ piece_map = {chess.PAWN: 0, chess.KNIGHT: 1, chess.BISHOP: 2, chess.ROOK: 3, chess.QUEEN: 4, chess.KING: 5}
 
 
 
 
 
38
  for sq, piece in board.piece_map().items():
39
  r, f = divmod(sq, 8)
40
  ch = piece_map[piece.piece_type] + (6 if piece.color == chess.BLACK else 0)
41
  tensor[0, ch, r, f] = 1.0
 
42
  return tensor
43
 
44
  def evaluate(self, board: chess.Board) -> float:
 
48
  score = float(output[0][0][0]) * 400.0
49
  return -score if board.turn == chess.BLACK else score
50
 
51
+ def order_moves(self, board, moves):
52
  scored = []
53
  for m in moves:
54
  s = 0
55
  if board.is_capture(m):
56
+ v, a = board.piece_at(m.to_square), board.piece_at(m.from_square)
 
57
  if v and a:
58
+ s = self.PIECE_VALUES.get(v.piece_type, 0) * 10 - self.PIECE_VALUES.get(a.piece_type, 0)
59
+ if m.promotion == chess.QUEEN: s += 90
 
 
60
  scored.append((s, m))
61
  scored.sort(key=lambda x: x[0], reverse=True)
62
  return [m for _, m in scored]
63
 
64
+ def alpha_beta(self, board, depth, alpha, beta) -> Tuple[float, Optional[chess.Move]]:
 
 
 
 
 
 
 
65
  if board.is_game_over():
66
  return (-10000 if board.is_checkmate() else 0), None
 
67
  if depth == 0:
68
  return self.evaluate(board), None
 
69
  moves = list(board.legal_moves)
70
+ if not moves: return 0, None
 
 
71
  moves = self.order_moves(board, moves)
72
+ best_move, best_score = moves[0], float('-inf')
 
 
 
73
  for move in moves:
74
  board.push(move)
75
  score, _ = self.alpha_beta(board, depth - 1, -beta, -alpha)
76
  score = -score
77
  board.pop()
 
78
  if score > best_score:
79
+ best_score, best_move = score, move
 
 
80
  alpha = max(alpha, score)
81
+ if alpha >= beta: break
 
 
82
  return best_score, best_move
83
 
84
  def search(self, fen: str, depth: int = 3):
85
  board = chess.Board(fen)
86
  self.nodes = 0
 
87
  moves = list(board.legal_moves)
88
  if len(moves) == 0:
89
  return {'best_move': '0000', 'evaluation': 0.0, 'nodes': 0, 'depth': 0}
 
90
  if len(moves) == 1:
91
+ return {'best_move': moves[0].uci(), 'evaluation': round(self.evaluate(board)/100, 2), 'nodes': 1, 'depth': 0}
92
+ best_move, best_score, current_depth = moves[0], float('-inf'), 1
 
 
 
 
 
 
 
 
 
93
  for d in range(1, depth + 1):
94
  try:
95
  score, move = self.alpha_beta(board, d, float('-inf'), float('inf'))
96
  if move:
97
+ best_move, best_score, current_depth = move, score, d
98
+ except: break
99
+ return {'best_move': best_move.uci(), 'evaluation': round(best_score/100, 2), 'depth': current_depth, 'nodes': self.nodes}
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ app = FastAPI(title="Nexus-Nano API", version="1.0.0")
102
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  engine = None
105
 
 
106
  class MoveRequest(BaseModel):
107
  fen: str
108
  depth: Optional[int] = Field(3, ge=1, le=5)
109
 
 
110
  class MoveResponse(BaseModel):
111
  best_move: str
112
  evaluation: float
 
114
  nodes_evaluated: int
115
  time_taken: int
116
 
 
117
  @app.on_event("startup")
118
  async def startup():
119
  global engine
120
+ logger.info("πŸš€ Starting Nexus-Nano...")
 
 
 
121
 
122
+ # FIXED: Check both possible paths
123
+ possible_paths = [
124
+ "/app/app/models/nexus-nano.onnx", # When uploaded to app/models/
125
+ "/app/models/nexus-nano.onnx" # When uploaded to models/
126
+ ]
127
 
128
+ model_path = None
129
+ for path in possible_paths:
130
+ if os.path.exists(path):
131
+ model_path = path
132
+ logger.info(f"βœ… Found model at: {path}")
133
+ break
 
 
 
 
134
 
135
+ if not model_path:
136
+ logger.error("❌ Model not found in any expected location")
137
+ logger.error(f"Checked paths: {possible_paths}")
138
+ # List all files
139
+ for root, dirs, files in os.walk("/app"):
140
+ for file in files:
141
+ if file.endswith('.onnx'):
142
+ logger.error(f"Found .onnx at: {os.path.join(root, file)}")
143
+ raise FileNotFoundError("Model not found")
144
 
145
  try:
146
  engine = NexusNanoEngine(model_path)
 
147
  except Exception as e:
148
  logger.error(f"❌ Load failed: {e}", exc_info=True)
149
  raise
150
 
 
151
  @app.get("/health")
152
  async def health():
153
+ return {"status": "healthy" if engine else "unhealthy", "model": "nexus-nano", "version": "1.0.0"}
 
 
 
 
 
 
 
154
 
155
  @app.post("/get-move", response_model=MoveResponse)
156
  async def get_move(req: MoveRequest):
157
+ if not engine: raise HTTPException(503, "Not loaded")
158
+ try: chess.Board(req.fen)
159
+ except: raise HTTPException(400, "Invalid FEN")
 
 
 
 
 
160
  start = time.time()
 
161
  try:
162
  result = engine.search(req.fen, req.depth)
163
  elapsed = int((time.time() - start) * 1000)
164
+ logger.info(f"βœ“ {result['best_move']} | {result['evaluation']:+.2f} | {elapsed}ms")
165
+ return MoveResponse(best_move=result['best_move'], evaluation=result['evaluation'],
166
+ depth_searched=result['depth'], nodes_evaluated=result['nodes'], time_taken=elapsed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  except Exception as e:
168
+ logger.error(f"Error: {e}", exc_info=True)
169
+ raise HTTPException(500, str(e))
 
170
 
171
  @app.get("/")
172
  async def root():
173
+ return {"name": "Nexus-Nano", "version": "1.0.0", "status": "online" if engine else "starting"}
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  if __name__ == "__main__":
176
  import uvicorn
177
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")