Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -169,164 +169,75 @@ Based *only* on the data in the DataFrame, provide the exact answer to the quest
|
|
| 169 |
|
| 170 |
def analyze_chess_image(file_path: str) -> str:
|
| 171 |
"""
|
| 172 |
-
Analyzes a chess position from an image using
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
"""
|
| 176 |
if not Path(file_path).is_file():
|
| 177 |
return f"ERROR: Chess image file not found at {file_path}"
|
| 178 |
|
| 179 |
try:
|
| 180 |
-
logging.info(f"Analyzing chess image: {file_path}")
|
| 181 |
|
| 182 |
# 1. Encode image to base64
|
| 183 |
with open(file_path, "rb") as image_file:
|
| 184 |
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
|
| 185 |
|
| 186 |
-
# 2. Use GPT-4o to get
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
prompt_messages = [
|
| 189 |
-
SystemMessage(content="You are a chess analysis assistant
|
| 190 |
HumanMessage(content=[
|
| 191 |
-
{
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
])
|
| 194 |
]
|
| 195 |
-
response = llm.invoke(prompt_messages)
|
| 196 |
-
fen_string = response.content.strip().replace('`', '') # Remove potential backticks
|
| 197 |
-
logging.info(f"Extracted FEN (raw): '{fen_string}'")
|
| 198 |
-
|
| 199 |
-
# Clean up FEN string - needs robust parsing
|
| 200 |
-
# Regex to capture full FEN: board turn castling enpassant halfmove fullmove
|
| 201 |
-
fen_match = re.search(r'([rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+\/[rnbqkpRNBQKP1-8]+)\s+([wb])\s+([-KQkq]+|\-)\s+([-a-h1-8]+|\-)\s+(\d+)\s+(\d+)', fen_string)
|
| 202 |
-
if not fen_match:
|
| 203 |
-
# Try simpler regex if full match fails (might miss some parts)
|
| 204 |
-
fen_match_simple = re.search(r'([rnbqkpRNBQKP1-8\/]+)\s+([wb])', fen_string)
|
| 205 |
-
if fen_match_simple:
|
| 206 |
-
board_part = fen_match_simple.group(1)
|
| 207 |
-
turn_part = fen_match_simple.group(2)
|
| 208 |
-
if board_part.count('/') == 7:
|
| 209 |
-
# Construct a potentially valid FEN, assuming standard defaults
|
| 210 |
-
# Crucially, the question states it IS Black's turn.
|
| 211 |
-
fen_string = f"{board_part} b - - 0 1"
|
| 212 |
-
logging.warning(f"Could only partially parse FEN, assuming defaults and forcing Black's turn: '{fen_string}'")
|
| 213 |
-
else:
|
| 214 |
-
logging.error(f"Failed to parse FEN: Board part invalid in '{fen_string}'.")
|
| 215 |
-
return "ERROR: Could not accurately determine the FEN string from the image (invalid board)."
|
| 216 |
-
else:
|
| 217 |
-
logging.error(f"Failed to parse FEN from image description: '{fen_string}'")
|
| 218 |
-
return "ERROR: Could not determine the FEN string from the image."
|
| 219 |
-
else:
|
| 220 |
-
fen_string = fen_match.group(0).strip() # Reconstruct matched FEN
|
| 221 |
-
logging.info(f"Successfully parsed FEN: '{fen_string}'")
|
| 222 |
-
|
| 223 |
-
# 3. Validate FEN and ensure it's Black's turn ('b')
|
| 224 |
-
try:
|
| 225 |
-
# Validate before potentially modifying turn
|
| 226 |
-
board_initial_check = chess.Board(fen_string)
|
| 227 |
-
fen_parts = fen_string.split(' ')
|
| 228 |
-
# Force turn to black as per question requirement
|
| 229 |
-
if fen_parts[1] != 'b':
|
| 230 |
-
logging.warning(f"FEN indicated '{fen_parts[1]}' turn, but question states Black's turn. Forcing turn to Black.")
|
| 231 |
-
fen_parts[1] = 'b'
|
| 232 |
-
# Clear en passant if it was White's turn (as en passant is only valid immediately after pawn move)
|
| 233 |
-
fen_parts[3] = '-'
|
| 234 |
-
corrected_fen = ' '.join(fen_parts)
|
| 235 |
-
board = chess.Board(corrected_fen)
|
| 236 |
-
logging.info(f"Corrected FEN for Black's turn: {board.fen()}")
|
| 237 |
-
else:
|
| 238 |
-
board = board_initial_check # Use originally parsed board if turn was already black
|
| 239 |
-
|
| 240 |
-
except ValueError as e:
|
| 241 |
-
logging.error(f"Invalid FEN generated or parsed: '{fen_string}'. Error: {e}")
|
| 242 |
-
# Try to see if the board part alone is valid
|
| 243 |
-
try:
|
| 244 |
-
board_part_only = fen_string.split(' ')[0]
|
| 245 |
-
if board_part_only.count('/') == 7:
|
| 246 |
-
test_board = chess.Board(f"{board_part_only} b - - 0 1")
|
| 247 |
-
logging.warning(f"Original FEN invalid, using board part only and forcing Black's turn: {test_board.fen()}")
|
| 248 |
-
board = test_board
|
| 249 |
-
else:
|
| 250 |
-
return f"ERROR: Invalid FEN string derived from image: {fen_string}"
|
| 251 |
-
except Exception:
|
| 252 |
-
return f"ERROR: Invalid FEN string derived from image: {fen_string}"
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
# 4. Use Stockfish engine to find the winning move
|
| 256 |
-
logging.info(f"Analyzing FEN with Stockfish: {board.fen()}")
|
| 257 |
-
engine = None # Initialize engine variable
|
| 258 |
-
try:
|
| 259 |
-
# Make sure the STOCKFISH_PATH environment variable is set correctly,
|
| 260 |
-
# or the stockfish executable is in the system's PATH.
|
| 261 |
-
engine = chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH)
|
| 262 |
-
|
| 263 |
-
# Analyze the position - search for a guaranteed win (mate).
|
| 264 |
-
# Set a reasonable time limit. Increase depth maybe?
|
| 265 |
-
# Let's try searching for mate specifically first.
|
| 266 |
-
# info = engine.analyse(board, chess.engine.Limit(time=5.0, depth=20), multipv=1) # Deeper search
|
| 267 |
-
analysis_result = engine.play(board, chess.engine.Limit(time=5.0, mate=1)) # Search specifically for mate in 1 first
|
| 268 |
-
|
| 269 |
-
if analysis_result.move is None or not board.is_legal(analysis_result.move):
|
| 270 |
-
# If no immediate mate, do a deeper search for best move
|
| 271 |
-
logging.info("No immediate mate found, performing deeper search...")
|
| 272 |
-
info = engine.analyse(board, chess.engine.Limit(time=5.0, depth=18), multipv=1) # Allow more time/depth
|
| 273 |
-
best_move = info[0].get('pv', [None])[0] if info else None
|
| 274 |
-
score = info[0].get('score') if info else None
|
| 275 |
-
else:
|
| 276 |
-
# Mate in 1 found
|
| 277 |
-
best_move = analysis_result.move
|
| 278 |
-
score = chess.engine.Mate(1) # Represent as mate score
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
if best_move is None:
|
| 282 |
-
logging.error("Stockfish analysis did not return a best move.")
|
| 283 |
-
return "ERROR: Chess engine analysis failed to find a move."
|
| 284 |
-
|
| 285 |
-
# Check score for confirmation of "guaranteed win"
|
| 286 |
-
is_win_confirmed = False
|
| 287 |
-
if score is not None:
|
| 288 |
-
pov_score = score.pov(chess.BLACK) # Score from Black's perspective
|
| 289 |
-
if pov_score.is_mate():
|
| 290 |
-
logging.info(f"Found winning mate ({pov_score.mate()}) for Black: {board.san(best_move)}")
|
| 291 |
-
is_win_confirmed = True
|
| 292 |
-
elif pov_score.score(mate_score=10000) is not None and pov_score.score(mate_score=10000) > 1000: # High centipawn advantage
|
| 293 |
-
logging.info(f"Found large advantage ({pov_score.score()} cp) for Black: {board.san(best_move)}")
|
| 294 |
-
is_win_confirmed = True
|
| 295 |
-
else:
|
| 296 |
-
logging.warning(f"Stockfish analysis score ({score}) does not guarantee a win, but returning best move found.")
|
| 297 |
-
else:
|
| 298 |
-
logging.warning("Stockfish analysis did not provide a score. Returning best move found.")
|
| 299 |
-
|
| 300 |
-
# Return the best move found in SAN format
|
| 301 |
-
san_move = board.san(best_move)
|
| 302 |
-
logging.info(f"Best move found for Black: {san_move}")
|
| 303 |
-
return san_move
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
|
|
|
| 326 |
|
| 327 |
except Exception as e:
|
| 328 |
-
logging.error(f"Unexpected error analyzing chess image {file_path}: {e}")
|
| 329 |
-
return f"ERROR: Unexpected error processing chess image. Details: {str(e)}"
|
| 330 |
|
| 331 |
|
| 332 |
def analyze_video_birds(file_path: str) -> str:
|
|
|
|
| 169 |
|
| 170 |
def analyze_chess_image(file_path: str) -> str:
|
| 171 |
"""
|
| 172 |
+
Analyzes a chess position from an image using GPT-4o directly.
|
| 173 |
+
Asks the model for the best move for Black that guarantees a win.
|
| 174 |
+
|
| 175 |
+
NOTE: Relies entirely on the LLM's image interpretation and chess evaluation,
|
| 176 |
+
which is significantly less reliable than a dedicated chess engine
|
| 177 |
+
for finding guaranteed winning moves.
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
The move in algebraic notation (if successful) or an error message.
|
| 181 |
"""
|
| 182 |
if not Path(file_path).is_file():
|
| 183 |
return f"ERROR: Chess image file not found at {file_path}"
|
| 184 |
|
| 185 |
try:
|
| 186 |
+
logging.info(f"Analyzing chess image using GPT-4o: {file_path}")
|
| 187 |
|
| 188 |
# 1. Encode image to base64
|
| 189 |
with open(file_path, "rb") as image_file:
|
| 190 |
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
|
| 191 |
|
| 192 |
+
# 2. Use GPT-4o to analyze and get the move
|
| 193 |
+
# Ensure OPENAI_API_KEY is set
|
| 194 |
+
if not os.getenv("OPENAI_API_KEY"):
|
| 195 |
+
return "ERROR: OPENAI_API_KEY not set. Cannot analyze chess image."
|
| 196 |
+
|
| 197 |
+
llm = ChatOpenAI(model="gpt-4o", max_tokens=50) # Limit tokens as we only want the move
|
| 198 |
prompt_messages = [
|
| 199 |
+
SystemMessage(content="You are a world-class chess analysis assistant observing a chess game."),
|
| 200 |
HumanMessage(content=[
|
| 201 |
+
{
|
| 202 |
+
"type": "text",
|
| 203 |
+
"text": "Analyze the chess position shown in the image. It is Black's turn to move. Determine the single best move for Black that guarantees a win. Respond with *only* the Standard Algebraic Notation (SAN) for this move (e.g., 'Qh4#', 'Nf3+', 'Rxe5'). Do not include *any* other text, explanation, or commentary."
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"type": "image_url",
|
| 207 |
+
"image_url": {
|
| 208 |
+
# Specify mime type if known, otherwise OpenAI often infers
|
| 209 |
+
"url": f"data:image/png;base64,{base64_image}"
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
])
|
| 213 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
logging.info("Sending chess image analysis request to GPT-4o...")
|
| 216 |
+
response = llm.invoke(prompt_messages)
|
| 217 |
+
move_san = response.content.strip()
|
| 218 |
+
|
| 219 |
+
# Basic validation: Check if the response is short and doesn't contain spaces
|
| 220 |
+
# (very crude check, as SAN can have checks/mates like '#', '+')
|
| 221 |
+
# A more robust check would involve trying to parse with a chess library,
|
| 222 |
+
# but we removed that dependency.
|
| 223 |
+
if not move_san:
|
| 224 |
+
logging.error("GPT-4o returned an empty response for chess analysis.")
|
| 225 |
+
return "ERROR: LLM analysis returned no move."
|
| 226 |
+
if ' ' in move_san or len(move_san) > 7: # Arbitrary length limit for typical SAN
|
| 227 |
+
logging.warning(f"GPT-4o chess response ('{move_san}') seems unusual (contains spaces or is long). Returning it as is, but it might be incorrect/include extra text.")
|
| 228 |
+
# Attempt to extract just the first 'word' if there are spaces
|
| 229 |
+
potential_move = move_san.split()[0]
|
| 230 |
+
# Further check if it contains only valid SAN characters? Maybe too complex.
|
| 231 |
+
# Let's return the potentially cleaned-up first part.
|
| 232 |
+
move_san = potential_move
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
logging.info(f"GPT-4o analysis returned potential move: '{move_san}'")
|
| 236 |
+
return move_san
|
| 237 |
|
| 238 |
except Exception as e:
|
| 239 |
+
logging.error(f"Unexpected error analyzing chess image {file_path} with GPT-4o: {e}", exc_info=True)
|
| 240 |
+
return f"ERROR: Unexpected error processing chess image with LLM. Details: {str(e)}"
|
| 241 |
|
| 242 |
|
| 243 |
def analyze_video_birds(file_path: str) -> str:
|