BryanBradfo commited on
Commit
05e856e
·
verified ·
1 Parent(s): 53f46f8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -97
app.py CHANGED
@@ -4,16 +4,15 @@ import chess
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
 
11
  # --- CONFIGURATION ---
12
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
  ELEVEN_API_KEY = os.getenv("ELEVEN_API_KEY")
14
 
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
 
@@ -34,32 +33,23 @@ def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
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},
@@ -67,7 +57,6 @@ def get_ai_move(board, level):
67
  "Grand Maître": {"time": 1.0, "skill": 20, "depth": 18}
68
  }
69
  config = levels.get(level, levels["Débutant"])
70
-
71
  try:
72
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
73
  engine.configure({"Skill Level": config["skill"]})
@@ -76,56 +65,68 @@ def get_ai_move(board, level):
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:
@@ -134,10 +135,10 @@ def process_turn(fen, level):
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(
@@ -149,74 +150,52 @@ def process_turn(fen, level):
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,
198
- game_mode=True,
199
- interactive=True
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
- )
 
4
  import chess.engine
5
  import pandas as pd
6
  import os
7
+ import uuid
8
+ import requests # On utilise requests direct, plus fiable que la lib elevenlabs
9
  from openai import OpenAI
 
10
 
11
  # --- CONFIGURATION ---
12
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
  ELEVEN_API_KEY = os.getenv("ELEVEN_API_KEY")
14
 
15
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
 
16
 
17
  STOCKFISH_PATH = "/usr/games/stockfish"
18
 
 
33
  OPENINGS_DB = _load_lichess_openings()
34
 
35
  # --- MOTEUR STOCKFISH ---
 
36
  def get_stockfish_analysis(board, time_limit=0.1):
 
37
  if not os.path.exists(STOCKFISH_PATH): return 0, "Inconnu"
38
  try:
39
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
40
  info = engine.analyse(board, chess.engine.Limit(time=time_limit))
41
  score = info["score"].white()
 
 
42
  if score.is_mate():
43
  score_val = f"Mat en {score.mate()}"
44
  else:
45
  score_val = score.score()
 
 
46
  best_move = info.get("pv", [None])[0]
47
  best_move_san = board.san(best_move) if best_move else "Aucun"
 
48
  return score_val, best_move_san
49
  except Exception as e:
50
+ return 0, "Erreur Engine"
 
51
 
52
  def get_ai_move(board, level):
 
53
  levels = {
54
  "Débutant": {"time": 0.01, "skill": 0, "depth": 1},
55
  "Intermédiaire": {"time": 0.1, "skill": 10, "depth": 5},
 
57
  "Grand Maître": {"time": 1.0, "skill": 20, "depth": 18}
58
  }
59
  config = levels.get(level, levels["Débutant"])
 
60
  try:
61
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
62
  engine.configure({"Skill Level": config["skill"]})
 
65
  except:
66
  return None
67
 
68
+ # --- AUDIO SANS LIBRAIRIE (HTTP REQUESTS) ---
69
+ def generate_voice_direct(text):
70
+ """Génère l'audio via requête HTTP directe pour éviter les bugs de librairie."""
71
+ if not ELEVEN_API_KEY:
72
+ return None, "ERREUR: Pas de clé API ElevenLabs configurée."
73
+ if not text: return None, "Pas de texte."
 
 
 
 
 
74
 
75
+ # ID de la voix Rachel (standard)
76
+ VOICE_ID = "21m00Tcm4TlvDq8ikWAM"
77
+
78
+ url = f"https://api.elevenlabs.io/v1/text-to-speech/{VOICE_ID}"
79
+
80
+ headers = {
81
+ "Accept": "audio/mpeg",
82
+ "Content-Type": "application/json",
83
+ "xi-api-key": ELEVEN_API_KEY
84
+ }
85
+
86
+ data = {
87
+ "text": text,
88
+ "model_id": "eleven_multilingual_v2",
89
+ "voice_settings": {
90
+ "stability": 0.5,
91
+ "similarity_boost": 0.75
92
+ }
93
+ }
94
 
95
  try:
96
+ response = requests.post(url, json=data, headers=headers)
 
 
 
 
 
97
 
98
+ if response.status_code == 200:
99
+ # Succès ! On sauvegarde
100
+ unique_filename = f"coach_{uuid.uuid4()}.mp3"
101
+ path = os.path.join("/tmp", unique_filename)
102
+ with open(path, "wb") as f:
103
+ f.write(response.content)
104
+ return path, None # Path, Pas d'erreur
105
+ else:
106
+ # Erreur renvoyée par ElevenLabs (ex: quota dépassé, mauvaise clé)
107
+ error_msg = f"Erreur ElevenLabs ({response.status_code}): {response.text}"
108
+ print(error_msg)
109
+ return None, error_msg
110
+
111
  except Exception as e:
112
+ return None, f"Exception Python: {str(e)}"
113
+
114
+ # --- PROMPT COACH ---
115
+ SYSTEM_PROMPT = """
116
+ Tu es Garry, un coach d'échecs légendaire.
117
+ Tu analyses le dernier coup du joueur (Blancs).
118
+ 1. Dis si le coup est bon ou mauvais.
119
+ 2. Donne un conseil tactique précis (ex: "Tu laisses ton Fou en prise", "Contrôle le centre").
120
+ 3. Sois bref (20 mots max).
121
+ """
122
 
123
  def process_turn(fen, level):
 
124
  if not fen: return fen, "", None, {}
125
 
126
  board = chess.Board(fen)
127
 
128
+ # Tour du joueur terminé (c'est aux noirs)
129
  if board.turn == chess.BLACK:
 
130
  # 1. Analyse Technique
131
  opening_name = "Inconnue"
132
  if not OPENINGS_DB.empty:
 
135
 
136
  score, best_move = get_stockfish_analysis(board)
137
 
138
+ # 2. LLM
139
+ llm_context = f"Ouverture: {opening_name}. Score: {score}. Meilleur coup aurait été: {best_move}."
 
140
  coach_text = "..."
141
+
142
  if openai_client:
143
  try:
144
  response = openai_client.chat.completions.create(
 
150
  )
151
  coach_text = response.choices[0].message.content
152
  except Exception as e:
153
+ coach_text = f"Erreur OpenAI: {e}"
154
  else:
155
+ coach_text = "Clé OpenAI manquante."
156
 
157
+ # 3. Audio (Méthode Directe)
158
+ audio_path, error_audio = generate_voice_direct(coach_text)
159
+
160
+ # Si erreur audio, on l'affiche dans le texte du coach pour debugger !
161
+ if error_audio:
162
+ coach_text += f"\n[DEBUG AUDIO]: {error_audio}"
163
 
164
  # 4. Debug Data
165
  debug_info = {
166
  "Ouverture": opening_name,
167
+ "Score": str(score),
168
+ "Conseil Engine": best_move,
169
+ "Audio Status": "OK" if audio_path else error_audio
170
  }
171
 
172
+ # 5. IA joue
173
  if not board.is_game_over():
174
  ai_move = get_ai_move(board, level)
175
+ if ai_move: board.push(ai_move)
 
176
 
 
177
  return board.fen(), coach_text, audio_path, debug_info
178
 
179
+ return fen, vous les Blancs !", None, {}
 
180
 
181
  # --- INTERFACE ---
 
182
  with gr.Blocks(title="ChessCoach Pro") as demo:
183
+ gr.Markdown("# ♟️ ChessCoach Pro - Entraînement Vocal")
 
 
 
 
 
184
 
185
  with gr.Row():
186
  with gr.Column(scale=2):
187
  level_radio = gr.Radio(
188
  ["Débutant", "Intermédiaire", "Avancé", "Grand Maître"],
189
+ label="Niveau IA", value="Débutant"
 
 
 
 
 
 
 
190
  )
191
+ board = Chessboard(label="Échiquier", value=chess.STARTING_FEN, game_mode=True, interactive=True)
192
 
193
  with gr.Column(scale=1):
194
+ coach_box = gr.Textbox(label="Conseils de Garry (et Erreurs)", interactive=False, lines=4)
195
  audio_box = gr.Audio(label="Voix", autoplay=True, interactive=False, type="filepath")
196
  debug_box = gr.JSON(label="Données Techniques")
197
 
198
+ board.move(fn=process_turn, inputs=[board, level_radio], outputs=[board, coach_box, audio_box, debug_box])
 
 
 
 
 
199
 
200
  if __name__ == "__main__":
201
+ demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False, allowed_paths=["/tmp"])