Gary Simmons commited on
Commit
bc3f6a6
Β·
1 Parent(s): 23fcdda

create chess tools and refactor tests and tools via domains

Browse files
tools/__init__.py β†’ __init__.py RENAMED
@@ -5,7 +5,14 @@ This package contains custom tools for the agent, including YouTube video analys
5
  and audio transcription capabilities.
6
  """
7
 
8
- from .youtube_tools import analyze_youtube_video, get_youtube_video_info
9
- from .transcription_tools import transcribe_audio
 
10
 
11
- __all__ = ["analyze_youtube_video", "get_youtube_video_info", "transcribe_audio"]
 
 
 
 
 
 
 
5
  and audio transcription capabilities.
6
  """
7
 
8
+ from libs.youtube.youtube_tools import analyze_youtube_video, get_youtube_video_info
9
+ from libs.transcription.transcription_tools import transcribe_audio
10
+ from libs.chess.chess_tools import analyze_chess_image, analyze_chess_position
11
 
12
+ __all__ = [
13
+ "analyze_youtube_video",
14
+ "get_youtube_video_info",
15
+ "transcribe_audio",
16
+ "analyze_chess_image",
17
+ "analyze_chess_position",
18
+ ]
app.py CHANGED
@@ -15,9 +15,10 @@ from smolagents import (
15
  WikipediaSearchTool,
16
  SpeechToTextTool,
17
  LiteLLMModel,
18
- tool,
19
  )
20
- from tools import analyze_youtube_video, get_youtube_video_info, transcribe_audio
 
 
21
 
22
 
23
  # (Keep Constants as is)
@@ -192,6 +193,8 @@ class BasicAgent:
192
  transcribe_audio,
193
  analyze_youtube_video,
194
  get_youtube_video_info,
 
 
195
  ],
196
  model=model,
197
  max_steps=20,
 
15
  WikipediaSearchTool,
16
  SpeechToTextTool,
17
  LiteLLMModel,
 
18
  )
19
+ from libs.chess.chess_tools import analyze_chess_image, analyze_chess_position
20
+ from libs.transcription.transcription_tools import transcribe_audio
21
+ from libs.youtube.youtube_tools import analyze_youtube_video, get_youtube_video_info
22
 
23
 
24
  # (Keep Constants as is)
 
193
  transcribe_audio,
194
  analyze_youtube_video,
195
  get_youtube_video_info,
196
+ analyze_chess_position,
197
+ analyze_chess_image
198
  ],
199
  model=model,
200
  max_steps=20,
libs/chess/chess_tools.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chess analysis tools for the Agents Course Final Assignment
3
+
4
+ Provides two tools for the agent:
5
+ - analyze_chess_position: analyze a chess position provided as FEN or PGN text
6
+ - analyze_chess_image: try to extract a FEN from an image (best-effort) and analyze it
7
+
8
+ The analysis uses python-chess for parsing and a small heuristic evaluation
9
+ so the tools do not require a native engine like Stockfish. If pytesseract is
10
+ available, the image tool will attempt OCR to find a FEN string inside the image.
11
+ """
12
+
13
+ from smolagents import tool
14
+ import io
15
+ import json
16
+ import tempfile
17
+ import re
18
+
19
+ try:
20
+ import chess
21
+ import chess.pgn
22
+ except Exception:
23
+ chess = None
24
+
25
+ try:
26
+ import cv2
27
+ import numpy as np
28
+ except Exception:
29
+ cv2 = None
30
+ np = None
31
+
32
+ try:
33
+ from PIL import Image
34
+ except Exception:
35
+ Image = None
36
+
37
+ try:
38
+ import pytesseract
39
+ except Exception:
40
+ pytesseract = None
41
+
42
+
43
+ PIECE_VALUES = (
44
+ {
45
+ chess.PAWN if chess else "p": 1,
46
+ chess.KNIGHT if chess else "n": 3,
47
+ chess.BISHOP if chess else "b": 3,
48
+ chess.ROOK if chess else "r": 5,
49
+ chess.QUEEN if chess else "q": 9,
50
+ chess.KING if chess else "k": 0,
51
+ }
52
+ if chess
53
+ else {}
54
+ )
55
+
56
+
57
+ def _simple_material_evaluation(board):
58
+ """Return material balance from White's perspective and a short breakdown."""
59
+ if chess is None:
60
+ raise RuntimeError("python-chess is required for position analysis")
61
+
62
+ values = {
63
+ chess.PAWN: 1,
64
+ chess.KNIGHT: 3,
65
+ chess.BISHOP: 3,
66
+ chess.ROOK: 5,
67
+ chess.QUEEN: 9,
68
+ chess.KING: 0,
69
+ }
70
+ white = 0
71
+ black = 0
72
+ counts = {}
73
+ for piece_type in values:
74
+ w = len(board.pieces(piece_type, chess.WHITE))
75
+ b = len(board.pieces(piece_type, chess.BLACK))
76
+ counts[piece_type] = (w, b)
77
+ white += w * values[piece_type]
78
+ black += b * values[piece_type]
79
+
80
+ balance = white - black
81
+ breakdown = {
82
+ chess.piece_name(pt): {
83
+ "white": counts[pt][0],
84
+ "black": counts[pt][1],
85
+ "value": values[pt],
86
+ }
87
+ for pt in counts
88
+ }
89
+ return balance, breakdown
90
+
91
+
92
+ def _format_moves(moves, max_moves=8):
93
+ return ", ".join([m for m in moves[:max_moves]])
94
+
95
+
96
+ @tool
97
+ def analyze_chess_position(position_text: str, max_moves: int = 8) -> str:
98
+ """
99
+ Analyze a chess position provided as FEN or PGN (single-game). Returns a JSON string
100
+ with basic diagnostics: legality, side to move, check/checkmate/stalemate, material
101
+ balance, and a short list of legal moves.
102
+
103
+ Args:
104
+ position_text: A FEN string, or a PGN string containing at least one game.
105
+ max_moves: How many top legal moves to list in the output (default 8).
106
+
107
+ Returns:
108
+ JSON string with analysis results.
109
+ """
110
+ try:
111
+ if chess is None:
112
+ return json.dumps(
113
+ {"status": "error", "error": "python-chess is not installed"}, indent=2
114
+ )
115
+
116
+ position_text = position_text.strip()
117
+
118
+ board = None
119
+
120
+ # Detect FEN: a FEN contains 6 space-separated fields (piece placement + side to move + ...)
121
+ if len(position_text.split()) >= 2 and "/" in position_text.split()[0]:
122
+ # Likely a FEN
123
+ try:
124
+ board = chess.Board(position_text.splitlines()[0])
125
+ except Exception:
126
+ # try using the full string
127
+ board = chess.Board(position_text)
128
+ else:
129
+ # Try PGN parsing (look for move text or headers)
130
+ try:
131
+ pgn_io = io.StringIO(position_text)
132
+ game = chess.pgn.read_game(pgn_io)
133
+ if game is None:
134
+ # Maybe the input is FEN without fields
135
+ board = chess.Board(position_text)
136
+ else:
137
+ # get the final position after the game
138
+ node = game
139
+ while node.variations:
140
+ node = node.variations[0]
141
+ board = node.board()
142
+ except Exception:
143
+ # Fallback: try to construct from the text directly
144
+ try:
145
+ board = chess.Board(position_text)
146
+ except Exception as e:
147
+ return json.dumps(
148
+ {"status": "error", "error": f"Could not parse FEN/PGN: {e}"},
149
+ indent=2,
150
+ )
151
+
152
+ # Now we have a board
153
+ legal_moves = [board.san(m) for m in board.legal_moves]
154
+ balance, breakdown = _simple_material_evaluation(board)
155
+
156
+ result = {
157
+ "status": "success",
158
+ "fen": board.fen(),
159
+ "turn": "white" if board.turn == chess.WHITE else "black",
160
+ "is_check": board.is_check(),
161
+ "is_checkmate": board.is_checkmate(),
162
+ "is_stalemate": board.is_stalemate(),
163
+ "is_insufficient_material": board.is_insufficient_material(),
164
+ "material_balance_white_minus_black": balance,
165
+ "material_breakdown": breakdown,
166
+ "legal_moves_count": (
167
+ board.legal_moves.count()
168
+ if hasattr(board.legal_moves, "count")
169
+ else len(list(board.legal_moves))
170
+ ),
171
+ "legal_moves_sample": _format_moves(legal_moves, max_moves=max_moves),
172
+ }
173
+
174
+ return json.dumps(result, indent=2)
175
+
176
+ except Exception as e:
177
+ return json.dumps({"status": "error", "error": str(e)}, indent=2)
178
+
179
+
180
+ @tool
181
+ def analyze_chess_image(image_bytes: bytes) -> str:
182
+ """
183
+ Best-effort: try to extract a FEN string from an image using OCR (if available),
184
+ and then analyze the position. If OCR is unavailable or fails, save the image to a
185
+ temporary file and return a helpful message describing how to use the `analyze_chess_position` tool.
186
+
187
+ Args:
188
+ image_bytes: Raw image bytes (PNG/JPEG/etc.)
189
+
190
+ Returns:
191
+ JSON string. If a FEN was found and parsed, returns the same output as `analyze_chess_position`.
192
+ Otherwise returns a helpful error/message and a temporary path where the image was saved.
193
+ """
194
+ try:
195
+ # First try OCR if pytesseract is available
196
+ if pytesseract and Image:
197
+ try:
198
+ img = Image.open(io.BytesIO(image_bytes)).convert("L")
199
+ text = pytesseract.image_to_string(img)
200
+ # Search for a FEN-like pattern: 8 ranks separated by '/'
201
+ fen_match = re.search(
202
+ r"([prnbqkPRNBQK1-8]+\/){7}[prnbqkPRNBQK1-8]+(?:\s[bBqQrRkK-]+.*)?",
203
+ text,
204
+ )
205
+ if fen_match:
206
+ fen_candidate = fen_match.group(0).strip()
207
+ # Try to analyze
208
+ return analyze_chess_position(fen_candidate)
209
+ # If no fen, maybe the OCR returns lines; attempt to find FEN in any line
210
+ for line in text.splitlines():
211
+ line = line.strip()
212
+ if not line:
213
+ continue
214
+ if "/" in line and len(line) >= 17:
215
+ # try parse
216
+ try:
217
+ return analyze_chess_position(line)
218
+ except Exception:
219
+ continue
220
+ # No FEN found via OCR
221
+ except Exception:
222
+ # OCR step failed; we'll fall back to saving image
223
+ pass
224
+
225
+ # Save image to temp file so a human (or another automated step) can inspect it
226
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
227
+ tmp.write(image_bytes)
228
+ tmp.flush()
229
+ tmp.close()
230
+
231
+ message = {
232
+ "status": "no_fen_found",
233
+ "message": "Could not automatically extract a FEN from the provided image."
234
+ " If the image includes a FEN string, install pytesseract and tesseract-ocr, or provide the FEN/PGN text directly",
235
+ "saved_image_path": tmp.name,
236
+ "how_to_use": "Call analyze_chess_position(fen_or_pgn) with a FEN string or PGN to get an analysis.",
237
+ }
238
+ return json.dumps(message, indent=2)
239
+
240
+ except Exception as e:
241
+ return json.dumps({"status": "error", "error": str(e)}, indent=2)
libs/chess/test_chess_tools.py ADDED
File without changes
{tests β†’ libs/transcription}/test_transcription_tools.py RENAMED
@@ -13,11 +13,14 @@ import struct
13
  import unittest
14
  from unittest.mock import Mock, patch, MagicMock
15
 
16
- # Add the parent directory to the path to import from tools
17
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 
 
18
 
19
  # Import the transcription tool directly to avoid YouTube tool dependencies
20
- from tools.transcription_tools import transcribe_audio
21
 
22
 
23
  class TestTranscriptionTools(unittest.TestCase):
@@ -55,7 +58,7 @@ class TestTranscriptionTools(unittest.TestCase):
55
 
56
  return wav_buffer.getvalue()
57
 
58
- @patch("tools.transcription_tools.SpeechToTextTool")
59
  def test_transcribe_audio_success(self, mock_speech_tool_class):
60
  """Test successful audio transcription."""
61
  # Setup mock
@@ -73,7 +76,7 @@ class TestTranscriptionTools(unittest.TestCase):
73
  mock_speech_tool_class.assert_called_once()
74
  mock_speech_tool.transcribe.assert_called_once_with(self.sample_audio_bytes)
75
 
76
- @patch("tools.transcription_tools.SpeechToTextTool")
77
  def test_transcribe_audio_empty_bytes(self, mock_speech_tool_class):
78
  """Test transcription with empty audio bytes."""
79
  # Setup mock
@@ -88,7 +91,7 @@ class TestTranscriptionTools(unittest.TestCase):
88
  self.assertEqual(result, "")
89
  mock_speech_tool.transcribe.assert_called_once_with(b"")
90
 
91
- @patch("tools.transcription_tools.SpeechToTextTool")
92
  def test_transcribe_audio_tool_exception(self, mock_speech_tool_class):
93
  """Test transcription when SpeechToTextTool raises an exception."""
94
  # Setup mock to raise exception
@@ -105,7 +108,7 @@ class TestTranscriptionTools(unittest.TestCase):
105
  self.assertIn("Failed to transcribe audio", str(context.exception))
106
  self.assertIn("Transcription service unavailable", str(context.exception))
107
 
108
- @patch("tools.transcription_tools.SpeechToTextTool")
109
  def test_transcribe_audio_invalid_format(self, mock_speech_tool_class):
110
  """Test transcription with invalid audio format."""
111
  # Setup mock to raise exception for invalid format
@@ -140,7 +143,7 @@ class TestTranscriptionTools(unittest.TestCase):
140
  # The function should at least be callable
141
  self.assertTrue(callable(transcribe_audio))
142
 
143
- @patch("tools.transcription_tools.SpeechToTextTool")
144
  def test_transcribe_audio_with_various_formats_description(
145
  self, mock_speech_tool_class
146
  ):
@@ -170,7 +173,7 @@ def test_basic_functionality():
170
 
171
  try:
172
  # Import the function to make sure it exists and imports work
173
- from tools.transcription_tools import transcribe_audio
174
 
175
  print("βœ… Successfully imported transcribe_audio function")
176
 
 
13
  import unittest
14
  from unittest.mock import Mock, patch, MagicMock
15
 
16
+ # Add the project root to the path so 'libs' can be imported (was appending 'libs' itself)
17
+ # This uses the directory three levels up from this test file (project root).
18
+ sys.path.append(
19
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20
+ )
21
 
22
  # Import the transcription tool directly to avoid YouTube tool dependencies
23
+ from libs.transcription.transcription_tools import transcribe_audio
24
 
25
 
26
  class TestTranscriptionTools(unittest.TestCase):
 
58
 
59
  return wav_buffer.getvalue()
60
 
61
+ @patch("libs.transcription.transcription_tools.SpeechToTextTool")
62
  def test_transcribe_audio_success(self, mock_speech_tool_class):
63
  """Test successful audio transcription."""
64
  # Setup mock
 
76
  mock_speech_tool_class.assert_called_once()
77
  mock_speech_tool.transcribe.assert_called_once_with(self.sample_audio_bytes)
78
 
79
+ @patch("libs.transcription.transcription_tools.SpeechToTextTool")
80
  def test_transcribe_audio_empty_bytes(self, mock_speech_tool_class):
81
  """Test transcription with empty audio bytes."""
82
  # Setup mock
 
91
  self.assertEqual(result, "")
92
  mock_speech_tool.transcribe.assert_called_once_with(b"")
93
 
94
+ @patch("libs.transcription.transcription_tools.SpeechToTextTool")
95
  def test_transcribe_audio_tool_exception(self, mock_speech_tool_class):
96
  """Test transcription when SpeechToTextTool raises an exception."""
97
  # Setup mock to raise exception
 
108
  self.assertIn("Failed to transcribe audio", str(context.exception))
109
  self.assertIn("Transcription service unavailable", str(context.exception))
110
 
111
+ @patch("libs.transcription.transcription_tools.SpeechToTextTool")
112
  def test_transcribe_audio_invalid_format(self, mock_speech_tool_class):
113
  """Test transcription with invalid audio format."""
114
  # Setup mock to raise exception for invalid format
 
143
  # The function should at least be callable
144
  self.assertTrue(callable(transcribe_audio))
145
 
146
+ @patch("libs.transcription.transcription_tools.SpeechToTextTool")
147
  def test_transcribe_audio_with_various_formats_description(
148
  self, mock_speech_tool_class
149
  ):
 
173
 
174
  try:
175
  # Import the function to make sure it exists and imports work
176
+ from libs.transcription.transcription_tools import transcribe_audio
177
 
178
  print("βœ… Successfully imported transcribe_audio function")
179
 
{tests β†’ libs/transcription}/test_transcription_tools_standalone.py RENAMED
@@ -14,8 +14,11 @@ import struct
14
  import unittest
15
  from unittest.mock import Mock, patch
16
 
17
- # Add the parent directory to the path to import from tools
18
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 
 
19
 
20
 
21
  def test_basic_functionality():
@@ -24,7 +27,7 @@ def test_basic_functionality():
24
 
25
  try:
26
  # Import the function directly to make sure it exists and imports work
27
- from tools.transcription_tools import transcribe_audio
28
 
29
  print("βœ… Successfully imported transcribe_audio function")
30
 
@@ -56,7 +59,7 @@ def test_transcribe_with_mock():
56
 
57
  try:
58
  # Import the function
59
- from tools.transcription_tools import transcribe_audio
60
 
61
  # Create sample audio bytes (simple WAV file structure)
62
  sample_rate = 44100
@@ -81,7 +84,7 @@ def test_transcribe_with_mock():
81
 
82
  # Mock the SpeechToTextTool
83
  with patch(
84
- "tools.transcription_tools.SpeechToTextTool"
85
  ) as mock_speech_tool_class:
86
  mock_speech_tool = Mock()
87
  mock_speech_tool.transcribe.return_value = (
@@ -115,11 +118,11 @@ def test_error_handling():
115
  print("Testing error handling...")
116
 
117
  try:
118
- from tools.transcription_tools import transcribe_audio
119
 
120
  # Mock the SpeechToTextTool to raise an exception
121
  with patch(
122
- "tools.transcription_tools.SpeechToTextTool"
123
  ) as mock_speech_tool_class:
124
  mock_speech_tool = Mock()
125
  mock_speech_tool.transcribe.side_effect = Exception(
 
14
  import unittest
15
  from unittest.mock import Mock, patch
16
 
17
+ # Add the project root to the path so 'libs' can be imported (was appending 'libs' itself)
18
+ # This uses the directory three levels up from this test file (project root).
19
+ sys.path.append(
20
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
+ )
22
 
23
 
24
  def test_basic_functionality():
 
27
 
28
  try:
29
  # Import the function directly to make sure it exists and imports work
30
+ from libs.transcription.transcription_tools import transcribe_audio
31
 
32
  print("βœ… Successfully imported transcribe_audio function")
33
 
 
59
 
60
  try:
61
  # Import the function
62
+ from libs.transcription.transcription_tools import transcribe_audio
63
 
64
  # Create sample audio bytes (simple WAV file structure)
65
  sample_rate = 44100
 
84
 
85
  # Mock the SpeechToTextTool
86
  with patch(
87
+ "libs.transcription.transcription_tools.SpeechToTextTool"
88
  ) as mock_speech_tool_class:
89
  mock_speech_tool = Mock()
90
  mock_speech_tool.transcribe.return_value = (
 
118
  print("Testing error handling...")
119
 
120
  try:
121
+ from libs.transcription.transcription_tools import transcribe_audio
122
 
123
  # Mock the SpeechToTextTool to raise an exception
124
  with patch(
125
+ "libs.transcription.transcription_tools.SpeechToTextTool"
126
  ) as mock_speech_tool_class:
127
  mock_speech_tool = Mock()
128
  mock_speech_tool.transcribe.side_effect = Exception(
{tools β†’ libs/transcription}/transcription_tools.py RENAMED
File without changes
{tests β†’ libs/youtube}/test_youtube_tools.py RENAMED
@@ -10,7 +10,7 @@ import os
10
 
11
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
 
13
- from tools.youtube_tools import get_youtube_video_info, analyze_youtube_video
14
 
15
 
16
  def test_video_info():
 
10
 
11
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
 
13
+ from libs.youtube.youtube_tools import get_youtube_video_info, analyze_youtube_video
14
 
15
 
16
  def test_video_info():
{scripts β†’ libs/youtube}/youtube_demo.py RENAMED
File without changes
{tools β†’ libs/youtube}/youtube_tools.py RENAMED
@@ -7,7 +7,7 @@ by extracting frames at intervals and analyzing their content.
7
 
8
  import json
9
  from smolagents import tool
10
- from tools.youtube_video_analyzer import (
11
  analyze_youtube_video_frames,
12
  get_video_metadata,
13
  )
 
7
 
8
  import json
9
  from smolagents import tool
10
+ from libs.youtube.youtube_video_analyzer import (
11
  analyze_youtube_video_frames,
12
  get_video_metadata,
13
  )
{tools β†’ libs/youtube}/youtube_video_analyzer.py RENAMED
@@ -58,7 +58,7 @@ def extract_video_frames(
58
  # Find the downloaded video file
59
  video_files = list(Path(temp_dir).glob("video.*"))
60
  if not video_files:
61
- raise Exception("No video file found after download")
62
 
63
  actual_video_path = str(video_files[0])
64
 
@@ -66,13 +66,15 @@ def extract_video_frames(
66
  cap = cv2.VideoCapture(actual_video_path)
67
 
68
  if not cap.isOpened():
69
- raise Exception("Could not open video file")
70
 
71
  fps = cap.get(cv2.CAP_PROP_FPS)
72
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
73
  duration = total_frames / fps if fps > 0 else 0
74
 
75
  frame_interval = int(fps * interval_seconds)
 
 
76
  frame_count = 0
77
  extracted_count = 0
78
 
 
58
  # Find the downloaded video file
59
  video_files = list(Path(temp_dir).glob("video.*"))
60
  if not video_files:
61
+ raise Exception("No video file found after download. The download may have failed or the video may be unavailable.")
62
 
63
  actual_video_path = str(video_files[0])
64
 
 
66
  cap = cv2.VideoCapture(actual_video_path)
67
 
68
  if not cap.isOpened():
69
+ raise Exception(f"Could not open video file: {actual_video_path}")
70
 
71
  fps = cap.get(cv2.CAP_PROP_FPS)
72
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
73
  duration = total_frames / fps if fps > 0 else 0
74
 
75
  frame_interval = int(fps * interval_seconds)
76
+ if frame_interval == 0:
77
+ frame_interval = 1
78
  frame_count = 0
79
  extracted_count = 0
80
 
requirements.txt CHANGED
@@ -14,4 +14,6 @@ yt-dlp
14
  openai-whisper
15
  torch
16
  transformers
17
- opencv-python
 
 
 
14
  openai-whisper
15
  torch
16
  transformers
17
+ opencv-python
18
+ python-chess>=1.9.0
19
+ pytesseract