Spaces:
Sleeping
Sleeping
mergedd getFen
Browse files- .gitattributes +2 -0
- .gitignore +1 -0
- Dockerfile +14 -0
- __pycache__/main.cpython-311.pyc +0 -0
- __pycache__/main.cpython-313.pyc +0 -0
- assets/openings_master.csv +0 -0
- main.py +40 -64
- models/SegModel (1).pt +3 -0
- models/chessDetection3d.pt +3 -0
- models/stockfish-windows-x86-64-avx2.exe +3 -0
- requirements.txt +68 -0
- routes/__pycache__/chess_review.cpython-313.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 +287 -0
- routes/detection.py +5 -1
- routes/segmentation.py +5 -1
.gitattributes
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.exe filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
api_tokens
|
Dockerfile
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9
|
2 |
+
|
3 |
+
# Set the working directory in the container
|
4 |
+
WORKDIR /app
|
5 |
+
|
6 |
+
COPY requirements.txt .
|
7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
8 |
+
|
9 |
+
# Copy everything from the current directory to the container
|
10 |
+
COPY . .
|
11 |
+
|
12 |
+
EXPOSE 7860
|
13 |
+
|
14 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
__pycache__/main.cpython-311.pyc
DELETED
Binary file (7.24 kB)
|
|
__pycache__/main.cpython-313.pyc
ADDED
Binary file (5.26 kB). View file
|
|
assets/openings_master.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
main.py
CHANGED
@@ -1,12 +1,24 @@
|
|
1 |
import io
|
|
|
|
|
2 |
from fastapi import FastAPI, File, UploadFile, Form
|
3 |
from fastapi.responses import JSONResponse, StreamingResponse
|
4 |
from PIL import Image, UnidentifiedImageError
|
5 |
from routes.segmentation import segment_chess_board
|
6 |
from routes.detection import detect_pieces
|
7 |
from routes.fen_generator import gen_fen
|
|
|
8 |
from typing import List, Dict, Any, Union
|
9 |
from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
app = FastAPI()
|
12 |
|
@@ -15,76 +27,13 @@ class DetectionResults(BaseModel):
|
|
15 |
confidences: list
|
16 |
classes: list
|
17 |
|
18 |
-
@app.get("/")
|
19 |
async def read_root():
|
20 |
return {
|
21 |
"name": "Narendra",
|
22 |
"age": 20,
|
23 |
"Gender": "Male"
|
24 |
}
|
25 |
-
|
26 |
-
@app.post("/getSeg")
|
27 |
-
async def get_seg(file: UploadFile = File(...)):
|
28 |
-
print(f'Image received: {file.filename}')
|
29 |
-
|
30 |
-
try:
|
31 |
-
image_content = await file.read()
|
32 |
-
if not image_content:
|
33 |
-
return JSONResponse(content={"error": "Empty file uploaded"}, status_code=400)
|
34 |
-
|
35 |
-
try:
|
36 |
-
image = Image.open(io.BytesIO(image_content))
|
37 |
-
except UnidentifiedImageError:
|
38 |
-
return JSONResponse(content={"error": "Invalid image format"}, status_code=400)
|
39 |
-
|
40 |
-
# If segment_chess_board is async, use `await`, otherwise remove `await`
|
41 |
-
segmented_image = await segment_chess_board(image)
|
42 |
-
|
43 |
-
if isinstance(segmented_image, dict):
|
44 |
-
return JSONResponse(content=segmented_image, status_code=400)
|
45 |
-
|
46 |
-
# Save to in-memory bytes
|
47 |
-
img_bytes = io.BytesIO()
|
48 |
-
segmented_image.save(img_bytes, format="PNG")
|
49 |
-
img_bytes.seek(0)
|
50 |
-
|
51 |
-
print("Image successfully processed and returned")
|
52 |
-
return StreamingResponse(
|
53 |
-
img_bytes,
|
54 |
-
media_type="image/png",
|
55 |
-
headers={"Content-Disposition": "inline; filename=output.png"}
|
56 |
-
)
|
57 |
-
|
58 |
-
|
59 |
-
except Exception as e:
|
60 |
-
return JSONResponse(content={"error": str(e)}, status_code=500)
|
61 |
-
|
62 |
-
|
63 |
-
@app.post("/getCoords")
|
64 |
-
async def get_coords(file: UploadFile = File(...)):
|
65 |
-
try:
|
66 |
-
image_content = await file.read()
|
67 |
-
|
68 |
-
if not image_content:
|
69 |
-
print("No image found")
|
70 |
-
return JSONResponse(content={"error": "Empty file uploaded"}, status_code=400)
|
71 |
-
|
72 |
-
try:
|
73 |
-
image = Image.open(io.BytesIO(image_content))
|
74 |
-
except UnidentifiedImageError:
|
75 |
-
return JSONResponse(content={"error": "Invalid image format"}, status_code=400)
|
76 |
-
|
77 |
-
detection_results = await detect_pieces(image)
|
78 |
-
|
79 |
-
if "error" in detection_results:
|
80 |
-
return JSONResponse(content=detection_results, status_code=400)
|
81 |
-
|
82 |
-
print("Image successfully processed and returned")
|
83 |
-
return JSONResponse(content={"detections": detection_results}, status_code=200)
|
84 |
-
|
85 |
-
except Exception as e:
|
86 |
-
print(f"Unexpected error: {str(e)}")
|
87 |
-
return JSONResponse(content={"error": "Unexpected error occurred", "details": str(e)}, status_code=500)
|
88 |
|
89 |
|
90 |
@app.post("/getFen")
|
@@ -125,3 +74,30 @@ async def get_fen(file : UploadFile = File(), perspective : str = Form("w"), nex
|
|
125 |
|
126 |
except Exception as e:
|
127 |
return JSONResponse(content={"error": "Unexpected error occurred", "details": str(e)}, status_code=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import io
|
2 |
+
import os
|
3 |
+
import tempfile
|
4 |
from fastapi import FastAPI, File, UploadFile, Form
|
5 |
from fastapi.responses import JSONResponse, StreamingResponse
|
6 |
from PIL import Image, UnidentifiedImageError
|
7 |
from routes.segmentation import segment_chess_board
|
8 |
from routes.detection import detect_pieces
|
9 |
from routes.fen_generator import gen_fen
|
10 |
+
from routes.chess_review import analyze_pgn
|
11 |
from typing import List, Dict, Any, Union
|
12 |
from pydantic import BaseModel
|
13 |
+
import asyncio
|
14 |
+
import sys
|
15 |
+
import tracemalloc
|
16 |
+
tracemalloc.start()
|
17 |
+
|
18 |
+
|
19 |
+
if sys.platform == "win32":
|
20 |
+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
21 |
+
|
22 |
|
23 |
app = FastAPI()
|
24 |
|
|
|
27 |
confidences: list
|
28 |
classes: list
|
29 |
|
30 |
+
@app.get("/test")
|
31 |
async def read_root():
|
32 |
return {
|
33 |
"name": "Narendra",
|
34 |
"age": 20,
|
35 |
"Gender": "Male"
|
36 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
|
39 |
@app.post("/getFen")
|
|
|
74 |
|
75 |
except Exception as e:
|
76 |
return JSONResponse(content={"error": "Unexpected error occurred", "details": str(e)}, status_code=500)
|
77 |
+
|
78 |
+
@app.post('/getReview')
|
79 |
+
async def getReview(file: UploadFile = File(...)):
|
80 |
+
print(os.getcwd())
|
81 |
+
print("call recieved")
|
82 |
+
|
83 |
+
if not file.filename.endswith(".pgn"):
|
84 |
+
return JSONResponse(content={"error": "Invalid file format. Please upload a PGN file"}, status_code=400)
|
85 |
+
|
86 |
+
try:
|
87 |
+
# Save the uploaded file to a temporary file
|
88 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pgn") as tmp_file:
|
89 |
+
tmp_file.write(await file.read())
|
90 |
+
tmp_file_path = tmp_file.name
|
91 |
+
|
92 |
+
# Analyze the PGN file
|
93 |
+
analysis_result = analyze_pgn(tmp_file_path)
|
94 |
+
|
95 |
+
# Clean up the temporary file
|
96 |
+
os.remove(tmp_file_path)
|
97 |
+
|
98 |
+
if not analysis_result:
|
99 |
+
return JSONResponse(content={"error": "No game found in the PGN file"}, status_code=400)
|
100 |
+
return analysis_result
|
101 |
+
|
102 |
+
except Exception as e:
|
103 |
+
return JSONResponse(content={"error": "Unexpected error occurred", "details": str(e)}, status_code=500)
|
models/SegModel (1).pt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:d577a96ac47f8a6222719816b4810cf4b207de17885599e6e068eacc36e252cf
|
3 |
+
size 6787281
|
models/chessDetection3d.pt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e1b2e9d61b0ea9f1ce81341d1aa7e54ff5721797925dbc960133d578021a0e4c
|
3 |
+
size 114439558
|
models/stockfish-windows-x86-64-avx2.exe
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:0b24c22f7894fa13ab27e32a29763055d0867dfb123d8763579dea5b7a91f419
|
3 |
+
size 79811584
|
requirements.txt
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
annotated-types==0.7.0
|
2 |
+
anyio==4.8.0
|
3 |
+
certifi==2025.1.31
|
4 |
+
charset-normalizer==3.4.1
|
5 |
+
chess==1.11.1
|
6 |
+
click==8.1.8
|
7 |
+
colorama==0.4.6
|
8 |
+
contourpy==1.3.1
|
9 |
+
cycler==0.12.1
|
10 |
+
dnspython==2.7.0
|
11 |
+
email_validator==2.2.0
|
12 |
+
fastapi==0.115.8
|
13 |
+
fastapi-cli==0.0.7
|
14 |
+
filelock==3.17.0
|
15 |
+
fonttools==4.56.0
|
16 |
+
fsspec==2025.2.0
|
17 |
+
h11==0.14.0
|
18 |
+
httpcore==1.0.7
|
19 |
+
httptools==0.6.4
|
20 |
+
httpx==0.28.1
|
21 |
+
idna==3.10
|
22 |
+
Jinja2==3.1.5
|
23 |
+
kiwisolver==1.4.8
|
24 |
+
markdown-it-py==3.0.0
|
25 |
+
MarkupSafe==3.0.2
|
26 |
+
matplotlib==3.10.0
|
27 |
+
mdurl==0.1.2
|
28 |
+
mpmath==1.3.0
|
29 |
+
networkx==3.4.2
|
30 |
+
numpy==2.1.1
|
31 |
+
opencv-python==4.11.0.86
|
32 |
+
packaging==24.2
|
33 |
+
pandas==2.2.3
|
34 |
+
pillow==11.1.0
|
35 |
+
psutil==7.0.0
|
36 |
+
py-cpuinfo==9.0.0
|
37 |
+
pydantic==2.10.6
|
38 |
+
pydantic_core==2.27.2
|
39 |
+
Pygments==2.19.1
|
40 |
+
pyparsing==3.2.1
|
41 |
+
python-dateutil==2.9.0.post0
|
42 |
+
python-dotenv==1.0.1
|
43 |
+
python-multipart==0.0.20
|
44 |
+
pytz==2025.1
|
45 |
+
PyYAML==6.0.2
|
46 |
+
requests==2.32.3
|
47 |
+
rich==13.9.4
|
48 |
+
rich-toolkit==0.13.2
|
49 |
+
scipy==1.15.2
|
50 |
+
seaborn==0.13.2
|
51 |
+
setuptools==75.8.0
|
52 |
+
shellingham==1.5.4
|
53 |
+
six==1.17.0
|
54 |
+
sniffio==1.3.1
|
55 |
+
starlette==0.45.3
|
56 |
+
sympy==1.13.1
|
57 |
+
torch==2.6.0
|
58 |
+
torchvision==0.21.0
|
59 |
+
tqdm==4.67.1
|
60 |
+
typer==0.15.1
|
61 |
+
typing_extensions==4.12.2
|
62 |
+
tzdata==2025.1
|
63 |
+
ultralytics==8.3.76
|
64 |
+
ultralytics-thop==2.0.14
|
65 |
+
urllib3==2.3.0
|
66 |
+
uvicorn==0.34.0
|
67 |
+
watchfiles==1.0.4
|
68 |
+
websockets==15.0
|
routes/__pycache__/chess_review.cpython-313.pyc
ADDED
Binary file (14.2 kB). View file
|
|
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
ADDED
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
17 |
+
MIDDLEGAME = "middlegame"
|
18 |
+
ENDGAME = "endgame"
|
19 |
+
|
20 |
+
class Classification(Enum):
|
21 |
+
BRILLIANT = "brilliant"
|
22 |
+
GREAT = "great"
|
23 |
+
BEST = "best"
|
24 |
+
EXCELLENT = "excellent"
|
25 |
+
GOOD = "good"
|
26 |
+
INACCURACY = "inaccuracy"
|
27 |
+
MISTAKE = "mistake"
|
28 |
+
MISS = "miss"
|
29 |
+
BLUNDER = "blunder"
|
30 |
+
BOOK = "book"
|
31 |
+
FORCED = "forced"
|
32 |
+
|
33 |
+
classification_values = {
|
34 |
+
Classification.BLUNDER: 0,
|
35 |
+
Classification.MISTAKE: 0.2,
|
36 |
+
Classification.MISS: 0.3,
|
37 |
+
Classification.INACCURACY: 0.4,
|
38 |
+
Classification.GOOD: 0.65,
|
39 |
+
Classification.EXCELLENT: 0.9,
|
40 |
+
Classification.BEST: 1,
|
41 |
+
Classification.GREAT: 1,
|
42 |
+
Classification.BRILLIANT: 1,
|
43 |
+
Classification.BOOK: 1,
|
44 |
+
Classification.FORCED: 1,
|
45 |
+
}
|
46 |
+
|
47 |
+
centipawn_classifications = [
|
48 |
+
Classification.BEST,
|
49 |
+
Classification.EXCELLENT,
|
50 |
+
Classification.GOOD,
|
51 |
+
Classification.INACCURACY,
|
52 |
+
Classification.MISS,
|
53 |
+
Classification.MISTAKE,
|
54 |
+
Classification.BLUNDER,
|
55 |
+
]
|
56 |
+
|
57 |
+
# Analysis parameters
|
58 |
+
FORCED_WIN_THRESHOLD = 500
|
59 |
+
MISS_CENTIPAWN_LOSS = 300
|
60 |
+
MISS_MATE_THRESHOLD = 3
|
61 |
+
ENDGAME_MATERIAL_THRESHOLD = 24
|
62 |
+
QUEEN_VALUE = 9
|
63 |
+
|
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, limit = chess.engine.Limit(time = 0.3))
|
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)
|
routes/detection.py
CHANGED
@@ -1,6 +1,10 @@
|
|
1 |
from ultralytics import YOLO
|
2 |
from PIL import Image
|
3 |
-
|
|
|
|
|
|
|
|
|
4 |
|
5 |
async def detect_pieces(image : Image):
|
6 |
if image is None:
|
|
|
1 |
from ultralytics import YOLO
|
2 |
from PIL import Image
|
3 |
+
import os
|
4 |
+
|
5 |
+
curr = os.getcwd()
|
6 |
+
detect_model_path = os.path.join(curr, 'models', 'chessDetection3d.pt')
|
7 |
+
detect_model = YOLO(detect_model_path)
|
8 |
|
9 |
async def detect_pieces(image : Image):
|
10 |
if image is None:
|
routes/segmentation.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1 |
from ultralytics import YOLO
|
2 |
from PIL import Image
|
|
|
3 |
|
4 |
-
|
|
|
|
|
|
|
5 |
|
6 |
async def segment_chess_board(image : Image):
|
7 |
if image is None:
|
|
|
1 |
from ultralytics import YOLO
|
2 |
from PIL import Image
|
3 |
+
import os
|
4 |
|
5 |
+
curr = os.getcwd()
|
6 |
+
seg_model_path = os.path.join(curr, 'models', 'SegModel (1).pt')
|
7 |
+
|
8 |
+
seg_model = YOLO(seg_model_path)
|
9 |
|
10 |
async def segment_chess_board(image : Image):
|
11 |
if image is None:
|