sabonzo commited on
Commit
a445487
·
verified ·
1 Parent(s): a34918a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +52 -141
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 a multimodal model (GPT-4o).
173
- Identifies the board state and then uses a chess engine to find the best move for Black.
174
- Returns the best move in algebraic notation or an error message.
 
 
 
 
 
 
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 FEN
187
- llm = ChatOpenAI(model="gpt-4o", max_tokens=200)
 
 
 
 
188
  prompt_messages = [
189
- SystemMessage(content="You are a chess analysis assistant. Analyze the provided chess board image."),
190
  HumanMessage(content=[
191
- {"type": "text", "text": "Describe the chess position shown in this image. Output *only* the Forsyth-Edwards Notation (FEN) string for this position, including side to move, castling rights, en passant target square, halfmove clock, and fullmove number. Assume standard algebraic notation rules (e.g., White pieces on ranks 1 & 2 initially). Determine the board orientation if possible, assuming the image shows the board from White's perspective unless clearly indicated otherwise."},
192
- {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}} # Specify image type if known (png/jpeg)
 
 
 
 
 
 
 
 
 
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
- except FileNotFoundError:
306
- logging.error(f"Stockfish engine not found at '{STOCKFISH_PATH}'. Please install Stockfish or set the STOCKFISH_PATH environment variable.")
307
- return f"ERROR: Stockfish engine not found at '{STOCKFISH_PATH}'"
308
- except chess.engine.EngineTerminatedError:
309
- logging.error("Chess engine terminated unexpectedly.")
310
- return "ERROR: Chess engine terminated unexpectedly."
311
- except Exception as e:
312
- logging.error(f"Error during chess engine analysis: {e}")
313
- if board and board.is_variant_end():
314
- logging.warning(f"Position is already game over: {board.result()}")
315
- return f"ERROR: Position is already game over ({board.result()}). No move possible."
316
- if board and not board.is_legal(best_move) and best_move is not None:
317
- logging.error(f"Engine suggested an illegal move: {best_move}")
318
- return "ERROR: Chess engine suggested an illegal move."
319
- # Check if the error indicates an illegal position from chess library
320
- if "invalid fen" in str(e).lower() or "illegal position" in str(e).lower():
321
- return f"ERROR: The derived FEN represents an illegal position: {board.fen() if board else fen_string}"
322
- return f"ERROR: Could not analyze chess position with engine. Details: {str(e)}"
323
- finally:
324
- if engine:
325
- engine.quit()
 
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: