BryanBradfo commited on
Commit
e0909e8
·
verified ·
1 Parent(s): 1a8dcf4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +151 -149
app.py CHANGED
@@ -7,14 +7,14 @@ import pandas as pd
7
  import os
8
  import uuid
9
  from openai import OpenAI
10
- import io
11
 
12
  # --- CONFIGURATION ---
13
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
15
  STOCKFISH_PATH = "/usr/games/stockfish"
16
 
17
- # --- DONNÉES OUVERTURES ---
18
  def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
19
  try:
20
  files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
@@ -30,65 +30,53 @@ def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
30
 
31
  OPENINGS_DB = _load_lichess_openings()
32
 
33
- # --- ANALYSE TECHNIQUE ET TRADUCTION ---
34
 
35
  def describe_move_human(board, move):
36
- """Traduit un coup UCI (e2e4) en phrase humaine explicite."""
37
- if not move: return "Rien de spécial"
38
-
39
  piece = board.piece_at(move.from_square)
40
  if not piece: return move.uci()
41
 
42
- piece_name = {
43
  chess.PAWN: "Pion", chess.KNIGHT: "Cavalier", chess.BISHOP: "Fou",
44
  chess.ROOK: "Tour", chess.QUEEN: "Dame", chess.KING: "Roi"
45
- }[piece.piece_type]
46
-
47
- dest_name = chess.square_name(move.to_square)
48
- is_capture = board.is_capture(move)
49
- is_check = board.gives_check(move)
50
-
51
- action = "mange sur" if is_capture else "va en"
52
- desc = f"{piece_name} {action} {dest_name}"
53
-
54
- if is_check: desc += " (Echec !)"
55
- return desc
56
 
57
- def get_engine_advice(board, time_limit=0.2):
58
- """Demande à Stockfish : 'Que doit faire le joueur (Blancs) maintenant ?'"""
59
- if not os.path.exists(STOCKFISH_PATH): return 0, "Inconnu", "Inconnu"
 
 
 
60
 
61
  try:
62
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
63
- # Analyse approfondie
64
  info = engine.analyse(board, chess.engine.Limit(time=time_limit))
65
  score = info["score"].white()
66
 
67
- # Score lisible
68
  if score.is_mate():
69
- score_val = f"Mat en {abs(score.mate())} coups"
70
- evaluation = "Victoire imminente" if score.mate() > 0 else "Tu vas perdre !"
71
  else:
72
- cp = score.score()
73
- score_val = str(cp)
74
- if cp > 100: evaluation = "Tu as l'avantage."
75
- elif cp < -100: evaluation = "Tu es en difficulté."
76
- else: evaluation = "Partie équilibrée."
77
-
78
- # Meilleur coup suggéré pour le joueur
79
  best_move = info.get("pv", [None])[0]
80
- if best_move:
81
- human_advice = describe_move_human(board, best_move)
82
- else:
83
- human_advice = "Aucun coup évident"
84
-
85
- return score_val, evaluation, human_advice
86
  except Exception as e:
87
  print(f"Engine Error: {e}")
88
- return 0, "Erreur", "Erreur"
89
 
90
  def get_ai_move(board, level):
91
- """L'IA (Noirs) joue."""
92
  levels = {
93
  "Débutant": {"time": 0.01, "skill": 1, "depth": 1},
94
  "Intermédiaire": {"time": 0.1, "skill": 8, "depth": 6},
@@ -104,152 +92,166 @@ def get_ai_move(board, level):
104
  except:
105
  return None
106
 
107
- # --- AUDIO OPENAI ---
108
- def generate_garry_voice(text):
 
 
 
 
 
 
 
 
 
 
 
 
109
  if not openai_client or not text: return None
110
  try:
111
  response = openai_client.audio.speech.create(
112
  model="tts-1",
113
- voice="fable", # Voix plus narrative/impérieuse
114
- input=text
 
115
  )
116
- unique_filename = f"garry_{uuid.uuid4()}.mp3"
117
- path = os.path.join("/tmp", unique_filename)
118
  response.stream_to_file(path)
119
  return path
120
  except:
121
  return None
122
 
123
- # --- INTELLIGENCE DU COACH ---
124
 
125
  SYSTEM_PROMPT = """
126
- Tu es Garry Kasparov. Tu coaches le joueur qui joue les BLANCS.
127
- L'adversaire (Noirs) est l'ordinateur.
128
 
129
- TES INSTRUCTIONS :
130
- 1. **Identifie toi aux Blancs** : Dis "Tu", "Nous". Ne parle des Noirs que comme "l'adversaire".
131
- 2. **Contextualise** : Utilise l'historique des coups pour comprendre le plan.
132
- 3. **Donne LA solution** : Je te fournis le "Meilleur coup calculé" par l'ordinateur. TA MISSION PRINCIPALE est de faire deviner ou d'expliquer ce coup au joueur sans jargon technique.
133
- - Au lieu de dire "Joue Cf3", dis "Sors ton cavalier pour contrôler le centre".
134
- - Au lieu de dire "Joue h3", dis "Fais une petite case de fuite pour ton Roi".
135
- 4. **État de la partie** : Si le score est mauvais, dis-lui comment tenir. Si le score est bon, dis-lui comment finir.
136
 
137
- Sois bref, incisif, encourageant. Tu es un champion du monde qui parle à un élève.
 
 
 
138
  """
139
 
140
- def process_game_cycle(fen, level, history_list):
141
  """
142
- 1. Joueur (Blancs) a joué.
143
- 2. Garry analyse la situation résultante.
144
- 3. Garry donne son avis + CONSEIL pour le PROCHAIN coup blanc.
145
- 4. IA (Noirs) répond.
146
  """
147
  board = chess.Board(fen)
148
 
149
- # Récupération de l'historique PGN pour le contexte
150
- pgn_game = chess.pgn.Game()
151
- node = pgn_game
152
- for move_uci in history_list:
153
- move = chess.Move.from_uci(move_uci)
154
- if move in node.board().legal_moves:
155
- node = node.add_variation(move)
156
 
157
- # Si c'est aux noirs, le joueur vient de jouer son coup blanc
158
- if board.turn == chess.BLACK:
159
- # On enregistre le dernier coup blanc dans l'historique (c'est le dernier move stack du board)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  try:
161
- last_white_move = board.peek()
162
- history_list.append(last_white_move.uci())
163
- except:
164
- pass
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- # --- 1. ANALYSE TECHNIQUE (Ce que voit Garry) ---
167
- opening = "Inconnue"
168
- if not OPENINGS_DB.empty:
169
- match = OPENINGS_DB[OPENINGS_DB["epd"] == board.epd()]
170
- if not match.empty: opening = match.iloc[0]['name']
171
-
172
- # On regarde le score actuel (pour juger le coup du joueur)
173
- score_val, evaluation, _ = get_engine_advice(board)
174
-
175
- # --- 2. IA JOUE (Noirs) ---
176
  ai_move = get_ai_move(board, level)
177
- ai_move_desc = "..."
178
- if ai_move:
179
- board.push(ai_move) # Les noirs ont joué
180
- history_list.append(ai_move.uci())
181
- ai_move_desc = describe_move_human(board, ai_move) # Description du coup noir
182
-
183
- # --- 3. CONSEIL POUR LE FUTUR (Ce que le joueur doit faire maintenant) ---
184
- # Maintenant c'est de nouveau aux Blancs. Quel est le meilleur coup ?
185
- _, _, best_move_human_desc = get_engine_advice(board)
186
-
187
- # --- 4. PRÉPARATION DU MESSAGE LLM ---
188
- pgn_exporter = chess.pgn.StringExporter(headers=False, variations=False, comments=False)
189
- pgn_string = pgn_game.accept(pgn_exporter)
190
-
191
- context = f"""
192
- [CONTEXTE]
193
- Tu es le coach des BLANCS.
194
- Historique de la partie : {pgn_string}
195
- Ouverture : {opening}.
196
-
197
- [SITUATION ACTUELLE]
198
- L'adversaire (Noirs) vient de répondre : {ai_move_desc}.
199
- C'est au joueur (Blancs) de jouer.
200
 
201
- [ANALYSE TECHNIQUE]
202
- Score : {score_val} ({evaluation}).
203
- LE MEILLEUR COUP À JOUER MAINTENANT POUR GAGNER EST : {best_move_human_desc}.
204
 
205
- [TA TÂCHE]
206
- 1. Commente brièvement la réponse des noirs.
207
- 2. Conseille fortement au joueur de jouer le "Meilleur coup" indiqué ci-dessus, mais explique-le avec des mots simples (stratégie), pas juste les coordonnées.
208
- """
209
-
210
- garry_text = "..."
211
- if openai_client:
212
- try:
213
- response = openai_client.chat.completions.create(
214
- model="gpt-4o-mini",
215
- messages=[
216
- {"role": "system", "content": SYSTEM_PROMPT},
217
- {"role": "user", "content": context}
218
- ]
219
- )
220
- garry_text = response.choices[0].message.content
221
- except Exception as e:
222
- garry_text = f"Erreur OpenAI: {e}"
223
-
224
- audio_path = generate_garry_voice(garry_text)
225
-
226
- return board.fen(), history_list, garry_text, audio_path
227
 
228
- return fen, history_list, "À toi de jouer les Blancs !", None
 
 
 
 
 
229
 
230
  # --- INTERFACE ---
231
 
232
- with gr.Blocks(title="ChessCoach Ultimate") as demo:
233
- gr.Markdown("# ♟️ ChessCoach Ultimate")
234
- gr.Markdown("Vous êtes les **Blancs**. Garry Kasparov vous regarde. Il analyse votre jeu et vous souffle les meilleurs coups à l'oreille.")
235
 
236
- # Variable d'état pour garder l'historique complet de la partie
237
- history_state = gr.State([])
238
 
239
  with gr.Row():
240
  with gr.Column(scale=2):
241
- level_select = gr.Dropdown(["Débutant", "Intermédiaire", "Avancé", "Grand Maître"], value="Débutant", label="Niveau IA Adverse")
242
- board = Chessboard(label="Partie en cours", value=chess.STARTING_FEN, game_mode=True, interactive=True)
243
-
 
 
 
 
244
  with gr.Column(scale=1):
245
- coach_box = gr.Textbox(label="Conseils de Garry", interactive=False, lines=5)
246
- audio_box = gr.Audio(label="Voix du Coach", autoplay=True, interactive=False, type="filepath")
 
247
 
248
- # Boucle de jeu
249
  board.move(
250
- fn=process_game_cycle,
251
- inputs=[board, level_select, history_state],
252
- outputs=[board, history_state, coach_box, audio_box]
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  )
254
 
255
  if __name__ == "__main__":
 
7
  import os
8
  import uuid
9
  from openai import OpenAI
10
+ import time
11
 
12
  # --- CONFIGURATION ---
13
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
14
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
15
  STOCKFISH_PATH = "/usr/games/stockfish"
16
 
17
+ # --- OUVERTURES ---
18
  def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
19
  try:
20
  files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
 
30
 
31
  OPENINGS_DB = _load_lichess_openings()
32
 
33
+ # --- OUTILS D'ANALYSE ---
34
 
35
  def describe_move_human(board, move):
36
+ """Traduction humaine du coup."""
37
+ if not move: return "Rien"
 
38
  piece = board.piece_at(move.from_square)
39
  if not piece: return move.uci()
40
 
41
+ piece_names = {
42
  chess.PAWN: "Pion", chess.KNIGHT: "Cavalier", chess.BISHOP: "Fou",
43
  chess.ROOK: "Tour", chess.QUEEN: "Dame", chess.KING: "Roi"
44
+ }
45
+ name = piece_names.get(piece.piece_type, "Pièce")
46
+ dest = chess.square_name(move.to_square)
47
+ capture = "mange sur" if board.is_capture(move) else "va en"
48
+ return f"{name} {capture} {dest}"
 
 
 
 
 
 
49
 
50
+ def get_engine_analysis(board, time_limit=0.6):
51
+ """
52
+ Analyse plus profonde (0.6s) pour mieux voir les menaces.
53
+ Retourne : Score, Description Humaine du Meilleur Coup, Description de la Menace (si on ne joue pas ce coup).
54
+ """
55
+ if not os.path.exists(STOCKFISH_PATH): return "0", "Inconnu"
56
 
57
  try:
58
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
59
+ # Analyse principale
60
  info = engine.analyse(board, chess.engine.Limit(time=time_limit))
61
  score = info["score"].white()
62
 
63
+ # Gestion du score
64
  if score.is_mate():
65
+ score_val = f"Mat en {abs(score.mate())}"
 
66
  else:
67
+ score_val = str(score.score())
68
+
69
+ # Meilleur coup pour NOUS (Blancs)
 
 
 
 
70
  best_move = info.get("pv", [None])[0]
71
+ best_move_desc = describe_move_human(board, best_move) if best_move else "Aucun coup sûr"
72
+
73
+ return score_val, best_move_desc
 
 
 
74
  except Exception as e:
75
  print(f"Engine Error: {e}")
76
+ return "0", "Erreur"
77
 
78
  def get_ai_move(board, level):
79
+ """L'adversaire (Noirs) joue."""
80
  levels = {
81
  "Débutant": {"time": 0.01, "skill": 1, "depth": 1},
82
  "Intermédiaire": {"time": 0.1, "skill": 8, "depth": 6},
 
92
  except:
93
  return None
94
 
95
+ # --- AUDIO & TRANSCRIPTION ---
96
+
97
+ def transcribe(audio_path):
98
+ """Transforme la voix du joueur en texte."""
99
+ if not openai_client or not audio_path: return None
100
+ try:
101
+ with open(audio_path, "rb") as f:
102
+ t = openai_client.audio.transcriptions.create(model="whisper-1", file=f, language="fr")
103
+ return t.text
104
+ except:
105
+ return None
106
+
107
+ def generate_voice(text):
108
+ """Génère la voix de Garry (Accélérée x1.25)."""
109
  if not openai_client or not text: return None
110
  try:
111
  response = openai_client.audio.speech.create(
112
  model="tts-1",
113
+ voice="fable",
114
+ input=text,
115
+ speed=1.25 # ACCÉLÉRATION DU COACH
116
  )
117
+ unique = f"garry_{uuid.uuid4()}.mp3"
118
+ path = os.path.join("/tmp", unique)
119
  response.stream_to_file(path)
120
  return path
121
  except:
122
  return None
123
 
124
+ # --- CERVEAU KASPAROV ---
125
 
126
  SYSTEM_PROMPT = """
127
+ Tu es Garry Kasparov, coach des Blancs.
128
+ Tu parles à un débutant.
129
 
130
+ RÈGLE D'OR DE STRUCTURE (RESPECTE L'ORDRE) :
131
+ 1. **L'ORDRE** : Dis D'ABORD le coup à jouer impérativement. (ex: "Sors ton Cavalier en f3 tout de suite !").
132
+ 2. **L'EXPLICATION** : ENSUITE, explique pourquoi en parlant du plan de l'adversaire (ex: "Sinon sa Dame va infiltrer ta défense et manger ton pion.").
 
 
 
 
133
 
134
+ TON :
135
+ - Dynamique, rapide, autoritaire mais bienveillant.
136
+ - Ne parle JAMAIS en coordonnées pures (ex: dis "Pion du Roi" pas "e4").
137
+ - Si le joueur pose une question, réponds précisément à sa question.
138
  """
139
 
140
+ def consult_garry(fen, user_audio=None, history_list=[], mode="auto"):
141
  """
142
+ Mode 'auto' : Garry commente après un coup.
143
+ Mode 'question' : Garry répond à la question vocale du joueur.
 
 
144
  """
145
  board = chess.Board(fen)
146
 
147
+ # 1. Analyse Technique
148
+ score, best_move_human = get_engine_analysis(board)
 
 
 
 
 
149
 
150
+ opening = "Inconnue"
151
+ if not OPENINGS_DB.empty:
152
+ match = OPENINGS_DB[OPENINGS_DB["epd"] == board.epd()]
153
+ if not match.empty: opening = match.iloc[0]['name']
154
+
155
+ # 2. Contexte pour le LLM
156
+ context = f"""
157
+ [SITUATION]
158
+ Trait aux : {'Blancs' if board.turn == chess.WHITE else 'Noirs'}.
159
+ Ouverture : {opening}.
160
+ Score : {score} (Positif=Bien pour nous).
161
+ MEILLEUR COUP CALCULÉ PAR STOCKFISH : {best_move_human}.
162
+ """
163
+
164
+ if mode == "question" and user_audio:
165
+ user_text = transcribe(user_audio)
166
+ context += f"\n[QUESTION DU JOUEUR] : '{user_text}'. Réponds à ça en t'aidant de l'analyse."
167
+ else:
168
+ context += f"\n[ACTION] : Le joueur attend tes instructions. Dis-lui de jouer le meilleur coup et explique la menace adverse."
169
+
170
+ # 3. Appel LLM
171
+ reply = "..."
172
+ if openai_client:
173
  try:
174
+ response = openai_client.chat.completions.create(
175
+ model="gpt-4o-mini",
176
+ messages=[
177
+ {"role": "system", "content": SYSTEM_PROMPT},
178
+ {"role": "user", "content": context}
179
+ ]
180
+ )
181
+ reply = response.choices[0].message.content
182
+ except Exception as e:
183
+ reply = f"Erreur IA: {e}"
184
+
185
+ # 4. Audio
186
+ audio_path = generate_voice(reply)
187
+
188
+ return reply, audio_path
189
 
190
+ # --- ORCHESTRATION DU JEU ---
191
+
192
+ def game_cycle(fen, level, history_list):
193
+ """Cycle : Joueur joue -> Garry conseille -> IA joue."""
194
+ board = chess.Board(fen)
195
+
196
+ # Si c'est aux noirs, le joueur vient de jouer
197
+ if board.turn == chess.BLACK:
198
+ # L'IA joue
 
199
  ai_move = get_ai_move(board, level)
200
+ if ai_move: board.push(ai_move)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ # Maintenant c'est aux blancs (nous)
203
+ # On demande conseil à Garry pour notre PROCHAIN coup
204
+ text, audio = consult_garry(board.fen(), mode="auto")
205
 
206
+ return board.fen(), history_list, text, audio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ return fen, history_list, "À vous !", None
209
+
210
+ def ask_question(fen, audio_file):
211
+ """Le joueur pose une question sans jouer."""
212
+ text, audio = consult_garry(fen, user_audio=audio_file, mode="question")
213
+ return text, audio
214
 
215
  # --- INTERFACE ---
216
 
217
+ with gr.Blocks(title="ChessCoach Ultimate V2") as demo:
218
+ gr.Markdown("# ♟️ ChessCoach Ultimate V2")
219
+ gr.Markdown("Coach : Garry Kasparov (Voix Rapide & Stratège). Jouez les Blancs.")
220
 
221
+ state_history = gr.State([])
 
222
 
223
  with gr.Row():
224
  with gr.Column(scale=2):
225
+ board = Chessboard(label="Jeu", value=chess.STARTING_FEN, game_mode=True, interactive=True)
226
+
227
+ with gr.Row():
228
+ # Bouton Micro
229
+ mic = gr.Audio(sources=["microphone"], type="filepath", label="Posez une question à Garry")
230
+ btn_ask = gr.Button("Envoyer la question")
231
+
232
  with gr.Column(scale=1):
233
+ level = gr.Dropdown(["Débutant", "Intermédiaire", "Avancé", "Grand Maître"], value="Débutant", label="Niveau Adversaire")
234
+ coach_txt = gr.Textbox(label="Conseil", interactive=False, lines=4)
235
+ coach_audio = gr.Audio(label="Voix", autoplay=True, interactive=False, type="filepath")
236
 
237
+ # 1. Jeu Automatique
238
  board.move(
239
+ fn=game_cycle,
240
+ inputs=[board, level, state_history],
241
+ outputs=[board, state_history, coach_txt, coach_audio]
242
+ )
243
+
244
+ # 2. Questions Vocales
245
+ # Quand on clique sur envoyer OU qu'on arrête de parler
246
+ btn_ask.click(
247
+ fn=ask_question,
248
+ inputs=[board, mic],
249
+ outputs=[coach_txt, coach_audio]
250
+ )
251
+ mic.stop_recording(
252
+ fn=ask_question,
253
+ inputs=[board, mic],
254
+ outputs=[coach_txt, coach_audio]
255
  )
256
 
257
  if __name__ == "__main__":