BryanBradfo commited on
Commit
15c5c47
·
verified ·
1 Parent(s): 6c81ee2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -89
app.py CHANGED
@@ -4,9 +4,9 @@ import chess
4
  import chess.engine
5
  import pandas as pd
6
  import os
 
7
  from openai import OpenAI
8
  from elevenlabs.client import ElevenLabs
9
- from collections import Counter
10
 
11
  # --- CONFIGURATION ---
12
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
@@ -15,13 +15,10 @@ ELEVEN_API_KEY = os.getenv("ELEVEN_API_KEY")
15
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
16
  eleven_client = ElevenLabs(api_key=ELEVEN_API_KEY) if ELEVEN_API_KEY else None
17
 
18
- # Chemin vers Stockfish (installé via Docker)
19
  STOCKFISH_PATH = "/usr/games/stockfish"
20
 
21
- # --- MOTEUR D'ANALYSE (Outils MCP) ---
22
-
23
  def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
24
- """Charge la base de données d'ouvertures."""
25
  try:
26
  files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
27
  dfs = []
@@ -31,79 +28,64 @@ def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
31
  dfs.append(df)
32
  if not dfs: return pd.DataFrame()
33
  return pd.concat(dfs, ignore_index=True)
34
- except Exception as e:
35
- print(f"Erreur chargement ouvertures: {e}")
36
  return pd.DataFrame()
37
 
38
- # Chargement unique au démarrage pour la performance
39
  OPENINGS_DB = _load_lichess_openings()
40
 
41
- def get_opening_info(board):
42
- """Trouve le nom de l'ouverture."""
43
- if OPENINGS_DB.empty: return "Base de données non chargée"
 
 
 
44
 
45
- epd = board.epd()
46
- match = OPENINGS_DB[OPENINGS_DB["epd"] == epd]
 
 
 
 
 
 
47
 
48
- if not match.empty:
49
- return f"{match.iloc[0]['eco']} - {match.iloc[0]['name']}"
50
- return "Ouverture inconnue ou transition"
51
-
52
- def get_stockfish_eval(fen, time_limit=0.1):
53
- """Utilise Stockfish pour évaluer la position (Centipawns)."""
54
- board = chess.Board(fen)
55
  try:
56
- if not os.path.exists(STOCKFISH_PATH):
57
- return "Stockfish non trouvé"
58
-
59
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
60
- info = engine.analyse(board, chess.engine.Limit(time=time_limit))
61
- score = info["score"].white()
62
 
63
- if score.is_mate():
64
- return f"Mat en {score.mate()}"
65
- return score.score()
66
  except Exception as e:
67
- return f"Erreur Engine: {e}"
 
68
 
69
- def analyze_full_context(fen):
70
- """Agrège toutes les données pour le LLM."""
71
- if not fen: return {}
72
  board = chess.Board(fen)
73
-
74
- # Matériel
75
- values = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, chess.QUEEN: 9}
76
- w_mat = sum(len(board.pieces(pt, chess.WHITE)) * val for pt, val in values.items())
77
- b_mat = sum(len(board.pieces(pt, chess.BLACK)) * val for pt, val in values.items())
78
-
79
- # Ouverture
80
- opening = get_opening_info(board)
81
-
82
- # Engine
83
- eval_score = get_stockfish_eval(fen)
84
-
85
- return {
86
- "turn": "White" if board.turn == chess.WHITE else "Black",
87
- "opening": opening,
88
- "material": {"white": w_mat, "black": b_mat, "diff": w_mat - b_mat},
89
- "stockfish_eval": eval_score,
90
- "is_check": board.is_check(),
91
- "is_game_over": board.is_game_over()
92
- }
93
 
94
- # --- COEUR DU COACH (IA & VOIX) ---
95
 
96
  SYSTEM_PROMPT = """
97
- Tu es Garry, un coach d'échecs légendaire, sarcastique mais brillant.
98
- Tu analyses la partie en direct.
99
- Basé sur les données JSON (Score Stockfish, Ouverture, Matériel) :
100
- 1. Si l'ouverture est reconnue, nomme-la avec classe.
101
- 2. Si Stockfish indique une erreur brutale (changement de score important), moque-toi gentiment.
102
- 3. Si le joueur trouve un bon coup, sois bref et approbateur.
103
- 4. Reste court (2 phrases max) pour l'audio.
104
  """
105
 
106
  def generate_voice(text):
 
107
  if not eleven_client or not text: return None
108
  try:
109
  audio_stream = eleven_client.generate(
@@ -111,51 +93,93 @@ def generate_voice(text):
111
  voice="Rachel",
112
  model="eleven_multilingual_v2"
113
  )
114
- path = "commentary.mp3"
115
- with open(path, "wb") as f:
116
  for chunk in audio_stream:
117
- f.write(chunk)
118
- return path
119
  except Exception as e:
120
  print(f"ElevenLabs Error: {e}")
121
  return None
122
 
123
- def game_loop(fen):
124
- """Boucle principale appelée à chaque coup."""
125
- if not fen: return "", None, {}
126
-
127
- # 1. Analyse Technique
128
- analysis_data = analyze_full_context(fen)
129
 
130
- # 2. Génération Texte (LLM)
131
- commentary = "Analyse..."
 
 
 
 
 
 
 
 
 
 
 
132
  if openai_client:
133
  try:
134
  response = openai_client.chat.completions.create(
135
  model="gpt-4o-mini",
136
  messages=[
137
  {"role": "system", "content": SYSTEM_PROMPT},
138
- {"role": "user", "content": f"Données techniques: {str(analysis_data)}"}
139
  ]
140
  )
141
  commentary = response.choices[0].message.content
142
  except Exception as e:
143
- commentary = f"Erreur LLM: {e}"
144
- else:
145
- commentary = "Clés API manquantes (OpenAI)."
146
-
147
- # 3. Génération Audio
148
  audio_path = generate_voice(commentary)
149
 
150
- return commentary, audio_path, analysis_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- # --- INTERFACE GRADIO ---
153
 
154
  with gr.Blocks(title="ChessCoach Pro") as demo:
155
- gr.Markdown("# ♟️ ChessCoach Pro (Docker + Stockfish + MCP)")
156
 
157
  with gr.Row():
158
  with gr.Column(scale=2):
 
 
 
 
 
 
 
159
  board = Chessboard(
160
  label="Échiquier",
161
  value=chess.STARTING_FEN,
@@ -164,16 +188,16 @@ with gr.Blocks(title="ChessCoach Pro") as demo:
164
  )
165
 
166
  with gr.Column(scale=1):
167
- coach_output = gr.Textbox(label="Coach Garry", interactive=False, lines=3)
168
  audio_output = gr.Audio(label="Voix", autoplay=True, interactive=False)
169
-
170
- with gr.Accordion("Données Techniques (MCP)", open=True):
171
- debug_json = gr.JSON(label="Analyse Temps Réel")
172
 
173
- # Événement : Quand on joue un coup
174
- board.move(fn=game_loop, inputs=[board], outputs=[coach_output, audio_output, debug_json])
 
 
 
 
175
 
176
- # --- LANCEMENT ---
177
- # IMPORTANT: ssr_mode=False est vital pour gradio_chessboard sur les versions récentes
178
  if __name__ == "__main__":
179
  demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
 
4
  import chess.engine
5
  import pandas as pd
6
  import os
7
+ import tempfile
8
  from openai import OpenAI
9
  from elevenlabs.client import ElevenLabs
 
10
 
11
  # --- CONFIGURATION ---
12
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
 
15
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
16
  eleven_client = ElevenLabs(api_key=ELEVEN_API_KEY) if ELEVEN_API_KEY else None
17
 
 
18
  STOCKFISH_PATH = "/usr/games/stockfish"
19
 
20
+ # --- CHARGEMENT DONNÉES (MCP) ---
 
21
  def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
 
22
  try:
23
  files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
24
  dfs = []
 
28
  dfs.append(df)
29
  if not dfs: return pd.DataFrame()
30
  return pd.concat(dfs, ignore_index=True)
31
+ except Exception:
 
32
  return pd.DataFrame()
33
 
 
34
  OPENINGS_DB = _load_lichess_openings()
35
 
36
+ # --- MOTEUR IA (JEU) ---
37
+
38
+ def get_ai_move(board, level):
39
+ """Fait jouer Stockfish selon le niveau."""
40
+ if not os.path.exists(STOCKFISH_PATH):
41
+ return None
42
 
43
+ # Configuration des niveaux
44
+ levels = {
45
+ "Débutant": {"time": 0.01, "skill": 0, "depth": 1},
46
+ "Intermédiaire": {"time": 0.1, "skill": 10, "depth": 5},
47
+ "Avancé": {"time": 0.5, "skill": 15, "depth": 10},
48
+ "Grand Maître": {"time": 1.0, "skill": 20, "depth": 18}
49
+ }
50
+ config = levels.get(level, levels["Débutant"])
51
 
 
 
 
 
 
 
 
52
  try:
 
 
 
53
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
54
+ # On configure le niveau de compétence (UCI option Skill Level 0-20)
55
+ engine.configure({"Skill Level": config["skill"]})
56
 
57
+ # On cherche le meilleur coup
58
+ result = engine.play(board, chess.engine.Limit(time=config["time"], depth=config["depth"]))
59
+ return result.move
60
  except Exception as e:
61
+ print(f"Erreur Stockfish Play: {e}")
62
+ return None
63
 
64
+ def get_stockfish_eval(fen):
65
+ """Analyse pure pour le coach."""
 
66
  board = chess.Board(fen)
67
+ try:
68
+ with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
69
+ info = engine.analyse(board, chess.engine.Limit(time=0.1))
70
+ score = info["score"].white()
71
+ if score.is_mate(): return f"Mat en {score.mate()}"
72
+ return score.score()
73
+ except:
74
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ # --- COACH (LLM & VOIX) ---
77
 
78
  SYSTEM_PROMPT = """
79
+ Tu es Garry, coach d'échecs.
80
+ 1. Analyse le DERNIER coup joué par le joueur (blancs).
81
+ 2. Si le score chute brutalement, moque-toi.
82
+ 3. Si l'ouverture est connue, dis-le.
83
+ 4. Reste très court (max 20 mots) pour l'audio.
84
+ 5. Ne commente pas le coup que l'ordinateur va jouer ensuite.
 
85
  """
86
 
87
  def generate_voice(text):
88
+ """Version corrigée avec fichier temporaire."""
89
  if not eleven_client or not text: return None
90
  try:
91
  audio_stream = eleven_client.generate(
 
93
  voice="Rachel",
94
  model="eleven_multilingual_v2"
95
  )
96
+ # Création d'un fichier temporaire sécurisé
97
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as fp:
98
  for chunk in audio_stream:
99
+ fp.write(chunk)
100
+ return fp.name
101
  except Exception as e:
102
  print(f"ElevenLabs Error: {e}")
103
  return None
104
 
105
+ def analyze_and_coach(fen):
106
+ """Génère le texte et l'audio."""
107
+ board = chess.Board(fen)
 
 
 
108
 
109
+ # Récupérer l'ouverture
110
+ opening = "Inconnue"
111
+ if not OPENINGS_DB.empty:
112
+ match = OPENINGS_DB[OPENINGS_DB["epd"] == board.epd()]
113
+ if not match.empty: opening = match.iloc[0]['name']
114
+
115
+ # Récupérer score
116
+ score = get_stockfish_eval(fen)
117
+
118
+ # Context pour le LLM
119
+ context = f"Ouverture: {opening}. Score (centipawns): {score}. Trait: {'Blancs' if board.turn == chess.WHITE else 'Noirs'}."
120
+
121
+ commentary = "..."
122
  if openai_client:
123
  try:
124
  response = openai_client.chat.completions.create(
125
  model="gpt-4o-mini",
126
  messages=[
127
  {"role": "system", "content": SYSTEM_PROMPT},
128
+ {"role": "user", "content": context}
129
  ]
130
  )
131
  commentary = response.choices[0].message.content
132
  except Exception as e:
133
+ commentary = f"Erreur IA: {e}"
134
+
 
 
 
135
  audio_path = generate_voice(commentary)
136
 
137
+ return commentary, audio_path, {"opening": opening, "score": score}
138
+
139
+ # --- BOUCLE PRINCIPALE ---
140
+
141
+ def game_step(fen, level):
142
+ """
143
+ 1. Le joueur vient de jouer (fen contient le coup du joueur).
144
+ 2. Le Coach analyse le coup du joueur.
145
+ 3. L'IA (Noirs) joue son coup.
146
+ 4. On renvoie le tout.
147
+ """
148
+ if not fen: return fen, "", None, {}
149
+
150
+ board = chess.Board(fen)
151
+
152
+ # Si c'est aux noirs de jouer, c'est que le joueur vient de jouer son coup
153
+ # Donc on lance le coach MAINTENANT sur la position actuelle
154
+ if board.turn == chess.BLACK:
155
+ coach_text, coach_audio, debug = analyze_and_coach(fen)
156
+
157
+ # Ensuite, l'IA joue (si la partie n'est pas finie)
158
+ if not board.is_game_over():
159
+ ai_move = get_ai_move(board, level)
160
+ if ai_move:
161
+ board.push(ai_move)
162
+
163
+ # On retourne : Nouvelle position (après coup IA), Texte Coach, Audio, Debug
164
+ return board.fen(), coach_text, coach_audio, debug
165
+
166
+ # Si c'est aux blancs, rien ne se passe (attente joueur)
167
+ return fen, "À vous de jouer !", None, {}
168
 
169
+ # --- INTERFACE ---
170
 
171
  with gr.Blocks(title="ChessCoach Pro") as demo:
172
+ gr.Markdown("# ♟️ ChessCoach Pro - Entraînement IA")
173
 
174
  with gr.Row():
175
  with gr.Column(scale=2):
176
+ # Sélecteur de niveau
177
+ level_selector = gr.Radio(
178
+ ["Débutant", "Intermédiaire", "Avancé", "Grand Maître"],
179
+ label="Niveau de l'IA (Noirs)",
180
+ value="Débutant"
181
+ )
182
+
183
  board = Chessboard(
184
  label="Échiquier",
185
  value=chess.STARTING_FEN,
 
188
  )
189
 
190
  with gr.Column(scale=1):
191
+ coach_output = gr.Textbox(label="Coach Garry", interactive=False, lines=2)
192
  audio_output = gr.Audio(label="Voix", autoplay=True, interactive=False)
193
+ debug_json = gr.JSON(label="Données MCP")
 
 
194
 
195
+ # Quand le joueur fait un move sur le plateau
196
+ board.move(
197
+ fn=game_step,
198
+ inputs=[board, level_selector],
199
+ outputs=[board, coach_output, audio_output, debug_json] # Note: board est output aussi pour afficher le coup de l'IA
200
+ )
201
 
 
 
202
  if __name__ == "__main__":
203
  demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)