BryanBradfo commited on
Commit
53f46f8
·
verified ·
1 Parent(s): 15c5c47

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -106
app.py CHANGED
@@ -4,7 +4,7 @@ import chess
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
 
@@ -17,7 +17,7 @@ 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")]
@@ -28,19 +28,38 @@ def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
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},
@@ -51,135 +70,128 @@ def get_ai_move(board, level):
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(
92
  text=text,
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,16 +200,23 @@ with gr.Blocks(title="ChessCoach Pro") as demo:
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)
 
 
 
 
 
 
 
 
4
  import chess.engine
5
  import pandas as pd
6
  import os
7
+ import uuid # Pour des noms de fichiers uniques
8
  from openai import OpenAI
9
  from elevenlabs.client import ElevenLabs
10
 
 
17
 
18
  STOCKFISH_PATH = "/usr/games/stockfish"
19
 
20
+ # --- DONNÉES OUVERTURES ---
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")]
 
28
  dfs.append(df)
29
  if not dfs: return pd.DataFrame()
30
  return pd.concat(dfs, ignore_index=True)
31
+ except:
32
  return pd.DataFrame()
33
 
34
  OPENINGS_DB = _load_lichess_openings()
35
 
36
+ # --- MOTEUR STOCKFISH ---
37
+
38
+ def get_stockfish_analysis(board, time_limit=0.1):
39
+ """Récupère le score et le meilleur coup suggéré."""
40
+ if not os.path.exists(STOCKFISH_PATH): return 0, "Inconnu"
41
+ try:
42
+ with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
43
+ info = engine.analyse(board, chess.engine.Limit(time=time_limit))
44
+ score = info["score"].white()
45
+
46
+ # Récupérer le score numérique
47
+ if score.is_mate():
48
+ score_val = f"Mat en {score.mate()}"
49
+ else:
50
+ score_val = score.score()
51
+
52
+ # Récupérer le meilleur coup (conseil)
53
+ best_move = info.get("pv", [None])[0]
54
+ best_move_san = board.san(best_move) if best_move else "Aucun"
55
+
56
+ return score_val, best_move_san
57
+ except Exception as e:
58
+ print(f"Stockfish Error: {e}")
59
+ return 0, "Erreur"
60
 
61
  def get_ai_move(board, level):
62
+ """L'IA joue son coup."""
 
 
 
 
63
  levels = {
64
  "Débutant": {"time": 0.01, "skill": 0, "depth": 1},
65
  "Intermédiaire": {"time": 0.1, "skill": 10, "depth": 5},
 
70
 
71
  try:
72
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
 
73
  engine.configure({"Skill Level": config["skill"]})
 
 
74
  result = engine.play(board, chess.engine.Limit(time=config["time"], depth=config["depth"]))
75
  return result.move
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  except:
77
+ return None
78
 
79
+ # --- CERVEAU DU COACH (LLM + AUDIO) ---
80
 
81
  SYSTEM_PROMPT = """
82
+ Tu es Garry, un coach d'échecs légendaire.
83
+ Tu analyses le dernier coup du joueur (Blancs) et la situation actuelle.
84
+ Tes objectifs :
85
+ 1. **Jugement** : Dis si le coup était bon ou mauvais.
86
+ 2. **Conseil** : Donne un conseil stratégique pour le prochain tour (ex: "Contrôle la colonne ouverte", "Attention à ton Cavalier", "Attaque le Roi").
87
+ 3. **Ton** : Pédagogique mais direct.
88
+ 4. **Format** : 2 phrases maximum. Très court pour l'audio.
89
  """
90
 
91
  def generate_voice(text):
92
+ """Génère l'audio avec un nom unique dans /tmp"""
93
+ if not eleven_client:
94
+ print("❌ Pas de clé ElevenLabs")
95
+ return None
96
+ if not text: return None
97
+
98
  try:
99
+ print(f"🎤 Génération audio pour : {text[:20]}...")
100
  audio_stream = eleven_client.generate(
101
  text=text,
102
  voice="Rachel",
103
  model="eleven_multilingual_v2"
104
  )
105
+
106
+ # Utilisation d'un UUID pour éviter le cache navigateur
107
+ unique_filename = f"coach_{uuid.uuid4()}.mp3"
108
+ path = os.path.join("/tmp", unique_filename)
109
+
110
+ with open(path, "wb") as f:
111
  for chunk in audio_stream:
112
+ f.write(chunk)
113
+
114
+ print(f"✅ Fichier audio créé : {path}")
115
+ return path
116
  except Exception as e:
117
+ print(f"❌ Erreur ElevenLabs: {e}")
118
  return None
119
 
120
+ def process_turn(fen, level):
121
+ """Orchestration du tour complet."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  if not fen: return fen, "", None, {}
123
 
124
  board = chess.Board(fen)
125
 
126
+ # Si c'est aux Noirs de jouer, le joueur (Blancs) vient de jouer
 
127
  if board.turn == chess.BLACK:
 
128
 
129
+ # 1. Analyse Technique
130
+ opening_name = "Inconnue"
131
+ if not OPENINGS_DB.empty:
132
+ match = OPENINGS_DB[OPENINGS_DB["epd"] == board.epd()]
133
+ if not match.empty: opening_name = match.iloc[0]['name']
134
+
135
+ score, best_move = get_stockfish_analysis(board)
136
+
137
+ # 2. Génération Commentaire (Coach)
138
+ llm_context = f"Ouverture: {opening_name}. Score actuel: {score}. Meilleur coup théorique était: {best_move}."
139
+
140
+ coach_text = "..."
141
+ if openai_client:
142
+ try:
143
+ response = openai_client.chat.completions.create(
144
+ model="gpt-4o-mini",
145
+ messages=[
146
+ {"role": "system", "content": SYSTEM_PROMPT},
147
+ {"role": "user", "content": llm_context}
148
+ ]
149
+ )
150
+ coach_text = response.choices[0].message.content
151
+ except Exception as e:
152
+ coach_text = f"Erreur LLM: {e}"
153
+ else:
154
+ coach_text = "Configurez votre clé OpenAI !"
155
+
156
+ # 3. Génération Audio
157
+ audio_path = generate_voice(coach_text)
158
+
159
+ # 4. Debug Data
160
+ debug_info = {
161
+ "Ouverture": opening_name,
162
+ "Score Centipawns": str(score),
163
+ "Conseil Stockfish": best_move
164
+ }
165
+
166
+ # 5. L'IA joue son coup (Noirs)
167
  if not board.is_game_over():
168
  ai_move = get_ai_move(board, level)
169
  if ai_move:
170
  board.push(ai_move)
171
 
172
+ # Retourne tout
173
+ return board.fen(), coach_text, audio_path, debug_info
174
+
175
+ # Si c'est au tour des Blancs (début de partie ou après reset)
176
+ return fen, "À vous de jouer les Blancs !", None, {}
177
 
178
  # --- INTERFACE ---
179
 
180
  with gr.Blocks(title="ChessCoach Pro") as demo:
181
+ gr.Markdown(
182
+ """
183
+ # ♟️ ChessCoach Pro
184
+ **Votre Coach IA personnel.** Jouez les Blancs. Garry analyse vos coups et vous donne des conseils stratégiques vocalement.
185
+ """
186
+ )
187
 
188
  with gr.Row():
189
  with gr.Column(scale=2):
190
+ level_radio = gr.Radio(
191
+ ["Débutant", "Intermédiaire", "Avancé", "Grand Maître"],
192
+ label="Niveau de l'adversaire",
 
193
  value="Débutant"
194
  )
 
195
  board = Chessboard(
196
  label="Échiquier",
197
  value=chess.STARTING_FEN,
 
200
  )
201
 
202
  with gr.Column(scale=1):
203
+ coach_box = gr.Textbox(label="Conseils de Garry", interactive=False, lines=4)
204
+ audio_box = gr.Audio(label="Voix", autoplay=True, interactive=False, type="filepath")
205
+ debug_box = gr.JSON(label="Données Techniques")
206
 
207
+ # Logique
208
  board.move(
209
+ fn=process_turn,
210
+ inputs=[board, level_radio],
211
+ outputs=[board, coach_box, audio_box, debug_box]
212
  )
213
 
214
  if __name__ == "__main__":
215
+ # ssr_mode=False est vital.
216
+ # allowed_paths=["/tmp"] permet à Gradio de lire les fichiers audio qu'on génère.
217
+ demo.launch(
218
+ server_name="0.0.0.0",
219
+ server_port=7860,
220
+ ssr_mode=False,
221
+ allowed_paths=["/tmp"]
222
+ )