Spaces:
Sleeping
Sleeping
Commit
·
b0a8dea
1
Parent(s):
7f4a7eb
added new api getfen which is working percfectly
Browse files- __pycache__/main.cpython-311.pyc +0 -0
- __pycache__/main.cpython-313.pyc +0 -0
- main.py +4 -3
- routes/__pycache__/chess_review.cpython-311.pyc +0 -0
- routes/__pycache__/chess_review_helper.cpython-313.pyc +0 -0
- routes/__pycache__/detection.cpython-311.pyc +0 -0
- routes/__pycache__/detection.cpython-313.pyc +0 -0
- routes/__pycache__/fen_generator.cpython-311.pyc +0 -0
- routes/__pycache__/fen_generator.cpython-313.pyc +0 -0
- routes/__pycache__/segmentation.cpython-311.pyc +0 -0
- routes/__pycache__/segmentation.cpython-313.pyc +0 -0
- routes/chess_review.py +193 -63
__pycache__/main.cpython-311.pyc
DELETED
Binary file (9.34 kB)
|
|
__pycache__/main.cpython-313.pyc
ADDED
Binary file (8.2 kB). View file
|
|
main.py
CHANGED
@@ -140,6 +140,7 @@ async def get_fen(file : UploadFile = File(), perspective : str = Form("w"), nex
|
|
140 |
|
141 |
@app.post('/getReview')
|
142 |
async def getReview(file: UploadFile = File(...)):
|
|
|
143 |
print("call recieved")
|
144 |
|
145 |
if not file.filename.endswith(".pgn"):
|
@@ -152,14 +153,14 @@ async def getReview(file: UploadFile = File(...)):
|
|
152 |
tmp_file_path = tmp_file.name
|
153 |
|
154 |
# Analyze the PGN file
|
155 |
-
analysis_result =
|
156 |
|
157 |
# Clean up the temporary file
|
158 |
os.remove(tmp_file_path)
|
159 |
|
160 |
if not analysis_result:
|
161 |
return JSONResponse(content={"error": "No game found in the PGN file"}, status_code=400)
|
162 |
-
|
163 |
-
|
164 |
except Exception as e:
|
165 |
return JSONResponse(content={"error": "Unexpected error occurred", "details": str(e)}, status_code=500)
|
|
|
140 |
|
141 |
@app.post('/getReview')
|
142 |
async def getReview(file: UploadFile = File(...)):
|
143 |
+
print(os.getcwd())
|
144 |
print("call recieved")
|
145 |
|
146 |
if not file.filename.endswith(".pgn"):
|
|
|
153 |
tmp_file_path = tmp_file.name
|
154 |
|
155 |
# Analyze the PGN file
|
156 |
+
analysis_result = analyze_pgn(tmp_file_path)
|
157 |
|
158 |
# Clean up the temporary file
|
159 |
os.remove(tmp_file_path)
|
160 |
|
161 |
if not analysis_result:
|
162 |
return JSONResponse(content={"error": "No game found in the PGN file"}, status_code=400)
|
163 |
+
return analysis_result
|
164 |
+
|
165 |
except Exception as e:
|
166 |
return JSONResponse(content={"error": "Unexpected error occurred", "details": str(e)}, status_code=500)
|
routes/__pycache__/chess_review.cpython-311.pyc
DELETED
Binary file (10.2 kB)
|
|
routes/__pycache__/chess_review_helper.cpython-313.pyc
ADDED
Binary file (14.2 kB). View file
|
|
routes/__pycache__/detection.cpython-311.pyc
DELETED
Binary file (1.5 kB)
|
|
routes/__pycache__/detection.cpython-313.pyc
ADDED
Binary file (1.57 kB). View file
|
|
routes/__pycache__/fen_generator.cpython-311.pyc
DELETED
Binary file (5.36 kB)
|
|
routes/__pycache__/fen_generator.cpython-313.pyc
ADDED
Binary file (4.62 kB). View file
|
|
routes/__pycache__/segmentation.cpython-311.pyc
DELETED
Binary file (1.27 kB)
|
|
routes/__pycache__/segmentation.cpython-313.pyc
ADDED
Binary file (1.3 kB). View file
|
|
routes/chess_review.py
CHANGED
@@ -1,47 +1,16 @@
|
|
1 |
-
import
|
|
|
|
|
2 |
import chess.pgn
|
3 |
import chess.engine
|
4 |
from enum import Enum
|
5 |
from typing import List, Dict
|
6 |
-
import
|
|
|
7 |
import json
|
8 |
-
import
|
9 |
-
import os
|
10 |
-
|
11 |
-
if sys.platform == "win32":
|
12 |
-
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
13 |
-
|
14 |
-
|
15 |
-
def load_opening_book(csv_path):
|
16 |
-
opening_book = {}
|
17 |
-
try:
|
18 |
-
with open(csv_path, newline='', encoding='utf-8') as csvfile:
|
19 |
-
reader = csv.reader(csvfile)
|
20 |
-
next(reader)
|
21 |
-
for row in reader:
|
22 |
-
if len(row) < 3:
|
23 |
-
continue
|
24 |
-
pgn_moves = row[2]
|
25 |
-
game = chess.pgn.Game()
|
26 |
-
board = game.board()
|
27 |
-
for move in pgn_moves.split():
|
28 |
-
if "." in move:
|
29 |
-
continue
|
30 |
-
try:
|
31 |
-
chess_move = board.push_san(move)
|
32 |
-
fen = " ".join(board.fen().split()[:4])
|
33 |
-
opening_book[fen] = chess_move.uci()
|
34 |
-
except ValueError:
|
35 |
-
break
|
36 |
-
except Exception as e:
|
37 |
-
print(f"Error loading opening book: {e}")
|
38 |
-
return opening_book
|
39 |
-
|
40 |
-
|
41 |
-
engine_path = os.path.join(os.getcwd(), "models", "stockfish_14_x64_avx2.exe")
|
42 |
-
csv_path = os.path.join(os.getcwd(), "assets", "opening_book.csv")
|
43 |
-
opening_book = load_opening_book(csv_path)
|
44 |
|
|
|
45 |
|
46 |
class GamePhase(Enum):
|
47 |
OPENING = "opening"
|
@@ -85,6 +54,7 @@ centipawn_classifications = [
|
|
85 |
Classification.BLUNDER,
|
86 |
]
|
87 |
|
|
|
88 |
FORCED_WIN_THRESHOLD = 500
|
89 |
MISS_CENTIPAWN_LOSS = 300
|
90 |
MISS_MATE_THRESHOLD = 3
|
@@ -94,64 +64,224 @@ QUEEN_VALUE = 9
|
|
94 |
def detect_game_phase(board: chess.Board, in_opening: bool) -> GamePhase:
|
95 |
if in_opening:
|
96 |
return GamePhase.OPENING
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
|
104 |
def is_book_move(board, opening_book, max_depth=8):
|
105 |
-
|
|
|
|
|
|
|
|
|
106 |
|
107 |
-
|
|
|
|
|
|
|
|
|
|
|
108 |
with open(pgn_file) as pgn:
|
109 |
game = chess.pgn.read_game(pgn)
|
|
|
110 |
if not game:
|
111 |
-
return
|
112 |
|
113 |
-
|
114 |
-
"
|
115 |
-
"
|
116 |
-
"
|
117 |
}
|
118 |
|
119 |
with chess.engine.SimpleEngine.popen_uci(engine_path) as engine:
|
120 |
board = game.board()
|
121 |
-
classifications = {
|
|
|
|
|
|
|
|
|
122 |
in_opening = True
|
123 |
|
124 |
for move_number, node in enumerate(game.mainline(), start=1):
|
|
|
125 |
pre_info = engine.analyse(board, chess.engine.Limit(depth=20))
|
126 |
pre_eval = pre_info["score"].white().score(mate_score=10000) or 0
|
127 |
best_move = pre_info.get("pv", [None])[0]
|
|
|
|
|
128 |
move = node.move
|
129 |
board.push(move)
|
|
|
|
|
130 |
post_info = engine.analyse(board, chess.engine.Limit(depth=20))
|
131 |
post_eval = post_info["score"].white().score(mate_score=10000) or 0
|
|
|
|
|
132 |
book_move = is_book_move(board, opening_book)
|
133 |
current_phase = detect_game_phase(board, in_opening)
|
134 |
if not book_move and in_opening:
|
135 |
in_opening = False
|
|
|
|
|
136 |
eval_loss = abs(pre_eval - post_eval)
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
player = "white" if board.turn == chess.BLACK else "black"
|
139 |
-
classifications[player][current_phase
|
140 |
-
|
|
|
|
|
|
|
141 |
"move_number": move_number,
|
142 |
-
"player":
|
143 |
"move": move.uci(),
|
144 |
"evaluation": post_eval / 100,
|
145 |
"evaluation_loss": eval_loss / 100,
|
146 |
"classification": classification.value
|
147 |
})
|
148 |
|
|
|
149 |
for phase in GamePhase:
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
|
157 |
-
return
|
|
|
1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
2 |
+
import os
|
3 |
+
import tempfile
|
4 |
import chess.pgn
|
5 |
import chess.engine
|
6 |
from enum import Enum
|
7 |
from typing import List, Dict
|
8 |
+
from datetime import datetime
|
9 |
+
import csv
|
10 |
import json
|
11 |
+
from fastapi.responses import JSONResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
+
app = FastAPI()
|
14 |
|
15 |
class GamePhase(Enum):
|
16 |
OPENING = "opening"
|
|
|
54 |
Classification.BLUNDER,
|
55 |
]
|
56 |
|
57 |
+
# Analysis parameters
|
58 |
FORCED_WIN_THRESHOLD = 500
|
59 |
MISS_CENTIPAWN_LOSS = 300
|
60 |
MISS_MATE_THRESHOLD = 3
|
|
|
64 |
def detect_game_phase(board: chess.Board, in_opening: bool) -> GamePhase:
|
65 |
if in_opening:
|
66 |
return GamePhase.OPENING
|
67 |
+
|
68 |
+
total_material = 0
|
69 |
+
queens = 0
|
70 |
+
|
71 |
+
for color in [chess.WHITE, chess.BLACK]:
|
72 |
+
for piece_type in chess.PIECE_TYPES:
|
73 |
+
if piece_type == chess.KING:
|
74 |
+
continue
|
75 |
+
|
76 |
+
count = len(board.pieces(piece_type, color))
|
77 |
+
value = {
|
78 |
+
chess.PAWN: 1,
|
79 |
+
chess.KNIGHT: 3,
|
80 |
+
chess.BISHOP: 3,
|
81 |
+
chess.ROOK: 5,
|
82 |
+
chess.QUEEN: QUEEN_VALUE
|
83 |
+
}[piece_type]
|
84 |
+
|
85 |
+
total_material += count * value
|
86 |
+
if piece_type == chess.QUEEN:
|
87 |
+
queens += count
|
88 |
+
|
89 |
+
endgame_conditions = [
|
90 |
+
total_material <= ENDGAME_MATERIAL_THRESHOLD,
|
91 |
+
queens == 0 and total_material <= ENDGAME_MATERIAL_THRESHOLD * 2,
|
92 |
+
]
|
93 |
+
|
94 |
+
return GamePhase.ENDGAME if any(endgame_conditions) else GamePhase.MIDDLEGAME
|
95 |
+
|
96 |
+
def get_evaluation_loss_threshold(classif: Classification, prev_eval: float) -> float:
|
97 |
+
prev_eval = abs(prev_eval)
|
98 |
+
if classif == Classification.BEST:
|
99 |
+
return max(0.0001 * prev_eval**2 + 0.0236 * prev_eval - 3.7143, 0)
|
100 |
+
elif classif == Classification.EXCELLENT:
|
101 |
+
return max(0.0002 * prev_eval**2 + 0.1231 * prev_eval + 27.5455, 0)
|
102 |
+
elif classif == Classification.GOOD:
|
103 |
+
return max(0.0002 * prev_eval**2 + 0.2643 * prev_eval + 60.5455, 0)
|
104 |
+
elif classif == Classification.INACCURACY:
|
105 |
+
return max(0.0002 * prev_eval**2 + 0.3624 * prev_eval + 108.0909, 0)
|
106 |
+
elif classif == Classification.MISS:
|
107 |
+
return max(0.00025 * prev_eval**2 + 0.38255 * prev_eval + 166.9541, 0)
|
108 |
+
elif classif == Classification.MISTAKE:
|
109 |
+
return max(0.0003 * prev_eval**2 + 0.4027 * prev_eval + 225.8182, 0)
|
110 |
+
else:
|
111 |
+
return float("inf")
|
112 |
+
|
113 |
+
def load_opening_book(csv_path):
|
114 |
+
opening_book = {}
|
115 |
+
try:
|
116 |
+
with open(csv_path, newline='', encoding='utf-8') as csvfile:
|
117 |
+
reader = csv.reader(csvfile)
|
118 |
+
next(reader)
|
119 |
+
for row in reader:
|
120 |
+
if len(row) < 3:
|
121 |
+
continue
|
122 |
+
pgn_moves = row[2]
|
123 |
+
game = chess.pgn.Game()
|
124 |
+
board = game.board()
|
125 |
+
for move in pgn_moves.split():
|
126 |
+
if "." in move:
|
127 |
+
continue
|
128 |
+
try:
|
129 |
+
chess_move = board.push_san(move)
|
130 |
+
fen = " ".join(board.fen().split()[:4])
|
131 |
+
opening_book[fen] = chess_move.uci()
|
132 |
+
except ValueError:
|
133 |
+
break
|
134 |
+
except Exception as e:
|
135 |
+
print(f"Error loading opening book: {e}")
|
136 |
+
return opening_book
|
137 |
|
138 |
def is_book_move(board, opening_book, max_depth=8):
|
139 |
+
if board.fullmove_number > max_depth:
|
140 |
+
return None
|
141 |
+
fen = " ".join(board.fen().split()[:4])
|
142 |
+
return opening_book.get(fen)
|
143 |
+
|
144 |
|
145 |
+
engine_path = os.path.join(os.getcwd(), "models", "stockfish-windows-x86-64-avx2.exe")
|
146 |
+
book_csv_path = os.path.join(os.getcwd(), "assets", "openings_master.csv")
|
147 |
+
|
148 |
+
def analyze_pgn(pgn_file: str) -> Dict:
|
149 |
+
opening_book = load_opening_book(book_csv_path)
|
150 |
+
|
151 |
with open(pgn_file) as pgn:
|
152 |
game = chess.pgn.read_game(pgn)
|
153 |
+
|
154 |
if not game:
|
155 |
+
return {"error": "No game found in the PGN file."}
|
156 |
|
157 |
+
result = {
|
158 |
+
"move_analysis": [],
|
159 |
+
"phase_analysis": {},
|
160 |
+
"player_summaries": {}
|
161 |
}
|
162 |
|
163 |
with chess.engine.SimpleEngine.popen_uci(engine_path) as engine:
|
164 |
board = game.board()
|
165 |
+
classifications = {
|
166 |
+
"white": {phase: [] for phase in GamePhase},
|
167 |
+
"black": {phase: [] for phase in GamePhase}
|
168 |
+
}
|
169 |
+
phase_data = {phase: [] for phase in GamePhase}
|
170 |
in_opening = True
|
171 |
|
172 |
for move_number, node in enumerate(game.mainline(), start=1):
|
173 |
+
# Analyze position before the move
|
174 |
pre_info = engine.analyse(board, chess.engine.Limit(depth=20))
|
175 |
pre_eval = pre_info["score"].white().score(mate_score=10000) or 0
|
176 |
best_move = pre_info.get("pv", [None])[0]
|
177 |
+
|
178 |
+
# Make the move
|
179 |
move = node.move
|
180 |
board.push(move)
|
181 |
+
|
182 |
+
# Analyze position after the move
|
183 |
post_info = engine.analyse(board, chess.engine.Limit(depth=20))
|
184 |
post_eval = post_info["score"].white().score(mate_score=10000) or 0
|
185 |
+
|
186 |
+
# Determine game phase
|
187 |
book_move = is_book_move(board, opening_book)
|
188 |
current_phase = detect_game_phase(board, in_opening)
|
189 |
if not book_move and in_opening:
|
190 |
in_opening = False
|
191 |
+
|
192 |
+
# Calculate evaluation loss
|
193 |
eval_loss = abs(pre_eval - post_eval)
|
194 |
+
|
195 |
+
# Initial classification
|
196 |
+
classification = Classification.BOOK if book_move else None
|
197 |
+
if not classification:
|
198 |
+
for classif in centipawn_classifications:
|
199 |
+
threshold = get_evaluation_loss_threshold(classif, pre_eval)
|
200 |
+
if eval_loss <= threshold:
|
201 |
+
classification = classif
|
202 |
+
break
|
203 |
+
classification = classification or Classification.BLUNDER
|
204 |
+
|
205 |
+
# Check for missed opportunities
|
206 |
+
is_winning = abs(pre_eval) >= FORCED_WIN_THRESHOLD
|
207 |
+
is_forced_win = pre_info["score"].is_mate() and pre_info["score"].relative.mate() <= MISS_MATE_THRESHOLD
|
208 |
+
if is_winning and move != best_move and (eval_loss >= MISS_CENTIPAWN_LOSS or is_forced_win):
|
209 |
+
classification = Classification.MISS
|
210 |
+
|
211 |
+
# Check for brilliant moves
|
212 |
+
if classification == Classification.BEST:
|
213 |
+
if pre_eval < -150 and post_eval >= 150:
|
214 |
+
classification = Classification.GREAT
|
215 |
+
elif pre_eval < -300 and post_eval >= 300:
|
216 |
+
classification = Classification.BRILLIANT
|
217 |
+
|
218 |
+
# Track classifications
|
219 |
player = "white" if board.turn == chess.BLACK else "black"
|
220 |
+
classifications[player][current_phase].append(classification)
|
221 |
+
phase_data[current_phase].append(classification)
|
222 |
+
|
223 |
+
# Add move analysis to result
|
224 |
+
result["move_analysis"].append({
|
225 |
"move_number": move_number,
|
226 |
+
"player": "White" if board.turn == chess.BLACK else "Black",
|
227 |
"move": move.uci(),
|
228 |
"evaluation": post_eval / 100,
|
229 |
"evaluation_loss": eval_loss / 100,
|
230 |
"classification": classification.value
|
231 |
})
|
232 |
|
233 |
+
# Phase analysis
|
234 |
for phase in GamePhase:
|
235 |
+
moves = phase_data[phase]
|
236 |
+
if moves:
|
237 |
+
rating = get_phase_rating(moves)
|
238 |
+
result["phase_analysis"][phase.value] = {
|
239 |
+
"rating": rating.value,
|
240 |
+
"move_count": len(moves)
|
241 |
+
}
|
242 |
+
|
243 |
+
# Player summaries
|
244 |
+
for color in ["white", "black"]:
|
245 |
+
player = game.headers["White" if color == "white" else "Black"]
|
246 |
+
counts = {c.value: 0 for c in Classification}
|
247 |
+
|
248 |
+
for phase in GamePhase:
|
249 |
+
phase_moves = classifications[color][phase]
|
250 |
+
for m in phase_moves:
|
251 |
+
counts[m.value] += 1
|
252 |
+
|
253 |
+
result["player_summaries"][player] = counts
|
254 |
+
|
255 |
+
def convert_enums(obj):
|
256 |
+
if isinstance(obj, Enum): # Convert Enum to its value
|
257 |
+
return obj.value
|
258 |
+
if isinstance(obj, dict): # Recursively handle dicts
|
259 |
+
return {k: convert_enums(v) for k, v in obj.items()}
|
260 |
+
if isinstance(obj, list): # Recursively handle lists
|
261 |
+
return [convert_enums(i) for i in obj]
|
262 |
+
return obj # Return other types as they are
|
263 |
+
|
264 |
+
json_result = convert_enums(result)
|
265 |
+
|
266 |
+
return JSONResponse(content=json_result)
|
267 |
+
|
268 |
+
|
269 |
+
def get_phase_rating(classified_moves: List[Classification]) -> Classification:
|
270 |
+
if not classified_moves:
|
271 |
+
return Classification.GOOD
|
272 |
+
|
273 |
+
total = sum(classification_values[m] for m in classified_moves)
|
274 |
+
average = total / len(classified_moves)
|
275 |
+
|
276 |
+
rating_order = [
|
277 |
+
(Classification.BRILLIANT, 0.95),
|
278 |
+
(Classification.GREAT, 0.85),
|
279 |
+
(Classification.BEST, 0.75),
|
280 |
+
(Classification.EXCELLENT, 0.65),
|
281 |
+
(Classification.GOOD, 0.5),
|
282 |
+
(Classification.INACCURACY, 0.35),
|
283 |
+
(Classification.MISS, 0.25),
|
284 |
+
(Classification.MISTAKE, 0.15)
|
285 |
+
]
|
286 |
|
287 |
+
return next((c for c, t in rating_order if average >= t), Classification.BLUNDER)
|