doctorlinux commited on
Commit
22b9557
·
verified ·
1 Parent(s): 5f40865

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +10 -13
  2. app.py +278 -179
  3. requirements.txt +9 -5
README.md CHANGED
@@ -1,13 +1,10 @@
1
- ---
2
- title: Ml
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
10
- license: gpl
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # Analizador de Partidas PGN (Doctor Linux)
2
+
3
+ - Repara PGN y analiza la primera partida válida (no se altera tu texto original).
4
+ - Si existe `models/blunder/model.joblib` + `features.txt`, muestra curva de **prob. de blunder**.
5
+ - Gráfico de Ventaja aprox. y reporte básico.
6
+
7
+ ## Local
8
+ ```bash
9
+ pip install -r requirements.txt
10
+ python app.py
 
 
 
app.py CHANGED
@@ -1,179 +1,278 @@
1
- import gradio as gr
2
- import chess
3
- import chess.pgn
4
- import matplotlib.pyplot as plt
5
- import numpy as np
6
- import io
7
- import re
8
- from io import BytesIO
9
-
10
- plt.switch_backend('Agg')
11
-
12
- class ReparadorPGN:
13
- @staticmethod
14
- def reparar_pgn(pgn_text):
15
- """Repara PGNs extremadamente corruptos"""
16
- lineas = pgn_text.split('\n')
17
- lineas_reparadas = []
18
-
19
- for linea in lineas:
20
- linea = linea.strip()
21
-
22
- # Reparar headers corruptos
23
- if linea.startswith('['):
24
- # Corregir comillas y paréntesis
25
- linea = re.sub(r"\[(.*?)[')](.*?)[')]", r"[\1 '\2']", linea)
26
- # Corregir nombres de headers
27
- linea = re.sub(r'\[Ulnite', '[White', linea)
28
- linea = re.sub(r'\[Result "I-0"\]', '[Result "1-0"]', linea)
29
- linea = re.sub(r'\[Result "O-I"\]', '[Result "0-1"]', linea)
30
- linea = re.sub(r'\[Result "I/2-I/2"\]', '[Result "1/2-1/2"]', linea)
31
-
32
- # Reparar notación de movimientos
33
- else:
34
- # Corregir piezas mal escritas
35
- correcciones = {
36
- r'\bnf3\b': 'Nf3', r'\bnc3\b': 'Nc3', r'\bng5\b': 'Ng5',
37
- r'\bhc6\b': 'Nc6', r'\bhf6\b': 'Nf6', r'\bhba\b': 'Nb8',
38
- r'\bhbe7\b': 'Nbd7', r'\bnn1\b': 'Nf1', r'\bhe2\b': 'Ne2',
39
- r'\bnh7\b': 'Nh7', r'\bhc5\b': 'Nc5', r'\bqu2\b': 'Qd2',
40
- r'\bre1\b': 'Re1', r'\brn\b': 'Rf1', r'\bbe4:\b': 'Be4',
41
- r'\bo-o-o\b': 'O-O-O', r'\bo-o\b': 'O-O'
42
- }
43
-
44
- for error, correccion in correcciones.items():
45
- linea = re.sub(error, correccion, linea, flags=re.IGNORECASE)
46
-
47
- lineas_reparadas.append(linea)
48
-
49
- return '\n'.join(lineas_reparadas)
50
-
51
- class AnalizadorAjedrez:
52
- def __init__(self):
53
- self.datos_jugadas = []
54
-
55
- def analizar_partida(self, pgn_string):
56
- """Analiza partida con múltiples intentos y reparación"""
57
- intentos = [
58
- pgn_string, # Intento 1: Original
59
- ReparadorPGN.reparar_pgn(pgn_string), # Intento 2: Reparado
60
- self._crear_pgn_minimo(pgn_string) # Intento 3: PGN mínimo
61
- ]
62
-
63
- for i, pgn_intento in enumerate(intentos):
64
- try:
65
- pgn = io.StringIO(pgn_intento)
66
- game = chess.pgn.read_game(pgn)
67
-
68
- if game and len(list(game.mainline_moves())) > 0:
69
- blancas = game.headers.get("White", "Anónimo")
70
- negras = game.headers.get("Black", "Anónimo")
71
- resultado = game.headers.get("Result", "*")
72
-
73
- # Analizar jugadas
74
- tablero = game.board()
75
- self.datos_jugadas = []
76
-
77
- for jugada_num, move in enumerate(game.mainline_moves(), 1):
78
- tablero.push(move)
79
- analisis = self._evaluar_posicion(tablero, jugada_num)
80
- self.datos_jugadas.append(analisis)
81
-
82
- print(f"✅ Análisis exitoso (Intento {i+1})")
83
- return blancas, negras, resultado, ""
84
-
85
- except Exception as e:
86
- continue
87
-
88
- return "Anónimo", "Anónimo", "*", "No se pudo analizar el PGN (formato muy corrupto)"
89
-
90
- def _crear_pgn_minimo(self, pgn_text):
91
- """Crea un PGN mínimo desde los movimientos"""
92
- # Extraer solo movimientos numéricos
93
- movimientos = re.findall(r'\d+\.\s*(\S+)\s+(\S+)', pgn_text)
94
- if not movimientos:
95
- movimientos = re.findall(r'\b([NBRQK]?[a-h]?[1-8]?x?[a-h][1-8]\+?\+?#?)\b', pgn_text, re.IGNORECASE)
96
-
97
- pgn_minimo = """[Event "Partida Reparada"]
98
- [White "Blancas"]
99
- [Black "Negras"]
100
- [Result "*"]
101
-
102
- """
103
- for i, (blanca, negra) in enumerate(movimientos[:50], 1): # Máximo 50 jugadas
104
- pgn_minimo += f"{i}. {blanca} {negra} "
105
-
106
- return pgn_minimo.strip()
107
-
108
- def _evaluar_posicion(self, tablero, jugada_num):
109
- """Evaluación simple pero robusta"""
110
- valores = {'p': 1, 'n': 3, 'b': 3, 'r': 5, 'q': 9, 'k': 0}
111
-
112
- material_blancas = sum(valores.get(p.symbol().lower(), 0)
113
- for p in tablero.piece_map().values()
114
- if p.color == chess.WHITE)
115
- material_negras = sum(valores.get(p.symbol().lower(), 0)
116
- for p in tablero.piece_map().values()
117
- if p.color == chess.BLACK)
118
-
119
- ventaja_material = material_blancas - material_negras
120
- movilidad = len(list(tablero.legal_moves))
121
-
122
- return {
123
- 'jugada': jugada_num,
124
- 'evaluacion': ventaja_material + (movilidad * 0.1),
125
- 'material_blancas': material_blancas,
126
- 'material_negras': material_negras,
127
- 'ventaja_material': ventaja_material,
128
- 'movilidad': movilidad
129
- }
130
-
131
- def generar_grafico(self, blancas, negras):
132
- """Genera gráfico incluso con datos mínimos"""
133
- if not self.datos_jugadas:
134
- # Gráfico de error informativo
135
- fig, ax = plt.subplots(figsize=(10, 6))
136
- ax.text(0.5, 0.5, '⚠️ PGN no analizable\n\nEl archivo PGN está corrupto\no tiene formato inválido',
137
- ha='center', va='center', fontsize=14, transform=ax.transAxes,
138
- bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral", alpha=0.7))
139
- ax.set_xlim(0, 1)
140
- ax.set_ylim(0, 1)
141
- ax.set_xticks([])
142
- ax.set_yticks([])
143
- return fig
144
-
145
- # Gráfico normal con datos
146
- jugadas = [d['jugada'] for d in self.datos_jugadas]
147
- evaluaciones = [d['evaluacion'] for d in self.datos_jugadas]
148
-
149
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
150
-
151
- ax1.plot(jugadas, evaluaciones, 'b-', linewidth=2)
152
- ax1.fill_between(jugadas, evaluaciones, alpha=0.3)
153
- ax1.axhline(y=0, color='black', linestyle='--')
154
- ax1.set_title(f'📊 Análisis: {blancas} vs {negras}', fontweight='bold')
155
- ax1.set_ylabel('Ventaja')
156
- ax1.grid(True, alpha=0.3)
157
-
158
- material = [d['ventaja_material'] for d in self.datos_jugadas]
159
- ax2.bar(jugadas, material, alpha=0.7, color=['green' if x >= 0 else 'red' for x in material])
160
- ax2.set_xlabel('Jugada')
161
- ax2.set_ylabel('Material')
162
- ax2.grid(True, alpha=0.3)
163
-
164
- plt.tight_layout()
165
- return fig
166
-
167
- def generar_reporte(self, blancas, negras, resultado):
168
- if not self.datos_jugadas:
169
- reporte = """## ❌ PGN NO ANALIZABLE
170
-
171
- **El archivo PGN está corrupto o tiene formato inválido.**
172
-
173
- ### 🔧 Posibles soluciones:
174
- 1. **Verifica que el PGN sea de una partida real**
175
- 2. **Comprueba que la notación de movimientos sea correcta**
176
- 3. **Intenta con otro archivo PGN**
177
- 4. **Los headers deben usar comillas: [Event "Nombre"]**
178
-
179
- ### 📝 Formato PGN correcto:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import chess
4
+ import chess.pgn
5
+ import matplotlib
6
+ matplotlib.use("Agg")
7
+ import matplotlib.pyplot as plt
8
+ import re
9
+ import joblib
10
+ import numpy as np
11
+ import pandas as pd
12
+ from io import StringIO
13
+
14
+ # ====== Intento cargar modelo de blunders si existe ======
15
+ BLUNDER_MODEL_PATH = "models/blunder/model.joblib"
16
+ BLUNDER_FEATURES_PATH = "models/blunder/features.txt"
17
+ blunder_model = None
18
+ blunder_features = None
19
+ if os.path.exists(BLUNDER_MODEL_PATH) and os.path.exists(BLUNDER_FEATURES_PATH):
20
+ try:
21
+ blunder_model = joblib.load(BLUNDER_MODEL_PATH)
22
+ with open(BLUNDER_FEATURES_PATH, "r", encoding="utf-8") as f:
23
+ blunder_features = [ln.strip() for ln in f if ln.strip()]
24
+ print("✅ Modelo de blunders cargado.")
25
+ except Exception as e:
26
+ print("⚠️ No se pudo cargar el modelo de blunders:", e)
27
+ blunder_model = None
28
+ blunder_features = None
29
+
30
+ # -------------------------------
31
+ # Reparador de PGN
32
+ # -------------------------------
33
+ class ReparadorPGN:
34
+ @staticmethod
35
+ def reparar_pgn(pgn_text: str) -> str:
36
+ if not isinstance(pgn_text, str):
37
+ return pgn_text
38
+ lineas = pgn_text.splitlines()
39
+ out = []
40
+ for linea in lineas:
41
+ original = linea
42
+ s = linea.strip()
43
+ if s.startswith("[") and "]" in s:
44
+ s = re.sub(r'\[([A-Za-z0-9_]+)\s+"([^"]*)["“”]?\]?$', r'[\1 "\2"]', s)
45
+ s = re.sub(r'\[Ulnite', '[White', s)
46
+ s = s.replace('[Result "I-0"]', '[Result "1-0"]')
47
+ s = s.replace('[Result "O-I"]', '[Result "0-1"]')
48
+ s = s.replace('[Result "I/2-I/2"]', '[Result "1/2-1/2"]')
49
+ out.append(s); continue
50
+ t = s
51
+ correcciones = {
52
+ r'\bnf([1-8a-h])': r'Nf\1',
53
+ r'\bnc([1-8a-h])': r'Nc\1',
54
+ r'\bng([1-8a-h])': r'Ng\1',
55
+ r'\bhc([1-8])\b': 'Nc\\1',
56
+ r'\bhf([1-8])\b': 'Nf\\1',
57
+ r'\bnn1\b': 'Nf1',
58
+ r'\bhe2\b': 'Ne2',
59
+ r'\bnh7\b': 'Nh7',
60
+ r'\bhc5\b': 'Nc5',
61
+ r'\bqu2\b': 'Qd2',
62
+ r'\bre1\b': 'Re1',
63
+ r'\brn\b': 'Rf1',
64
+ r'\bbe4:?': 'Be4',
65
+ r'\bo-o-o\b': 'O-O-O',
66
+ r'\bo-o\b': 'O-O',
67
+ }
68
+ for pat, rep in correcciones.items():
69
+ t = re.sub(pat, rep, t, flags=re.IGNORECASE)
70
+ out.append(t if t else original)
71
+ texto = "\n".join(out)
72
+ texto = texto.replace('Result "* *"', 'Result "*"')
73
+ return texto
74
+
75
+ # -------------------------------
76
+ # Analizador simple
77
+ # -------------------------------
78
+ class AnalizadorAjedrez:
79
+ def __init__(self):
80
+ self.datos_jugadas = [] # lista de dicts por ply
81
+ self._last_meta = {} # meta de partida analizada
82
+
83
+ def analizar_primera_partida_valida(self, pgn_text: str):
84
+ if not pgn_text or not pgn_text.strip():
85
+ return "Anónimo", "Anónimo", "*", "PGN vacío.", None
86
+ candidatos = []
87
+ candidatos.append(("original", pgn_text))
88
+ reparado = ReparadorPGN.reparar_pgn(pgn_text)
89
+ candidatos.append(("reparado", reparado))
90
+ candidatos.append(("minimo", self._crear_pgn_minimo(pgn_text)))
91
+ for etiqueta, intento in candidatos:
92
+ try:
93
+ partidas = list(self._iterar_partidas(intento))
94
+ for game in partidas:
95
+ if game is None: continue
96
+ plies = sum(1 for _ in game.mainline_moves())
97
+ if plies == 0: continue
98
+ blancas = game.headers.get("White", "Anónimo") or "Anónimo"
99
+ negras = game.headers.get("Black", "Anónimo") or "Anónimo"
100
+ resultado = game.headers.get("Result", "*") or "*"
101
+ self._analizar_jugadas(game, blancas, negras)
102
+ info = f"Análisis exitoso ({etiqueta}). Plies: {plies}"
103
+ return blancas, negras, resultado, info, intento
104
+ except Exception:
105
+ continue
106
+ return "Anónimo", "Anónimo", "*", "No se pudo analizar ninguna partida válida en el PGN.", None
107
+
108
+ def _iterar_partidas(self, pgn_text: str):
109
+ f = StringIO(pgn_text)
110
+ while True:
111
+ game = chess.pgn.read_game(f)
112
+ if game is None: break
113
+ yield game
114
+
115
+ def _analizar_jugadas(self, game, white, black):
116
+ board = game.board()
117
+ self.datos_jugadas = []
118
+ self._last_meta = {"white": white, "black": black}
119
+ jugada_num = 0
120
+ for mv in game.mainline_moves():
121
+ jugada_num += 1
122
+ board.push(mv)
123
+ self.datos_jugadas.append(self._evaluar_posicion(board, jugada_num, white, black))
124
+
125
+ def _crear_pgn_minimo(self, pgn_text: str) -> str:
126
+ pares = re.findall(r'\b(\d+)\.\s*([^\s]+)\s+([^\s]+)', pgn_text)
127
+ movimientos = []
128
+ if pares:
129
+ for _, m1, m2 in pares[:50]:
130
+ movimientos.append((m1, m2))
131
+ else:
132
+ san = re.findall(r'\b([NBRQK]?[a-h]?[1-8]?x?[a-h][1-8](?:=[NBRQK])?[+#]?)\b', pgn_text)
133
+ if san:
134
+ it = iter(san[:100]); tmp = []
135
+ for m in it:
136
+ n1 = m; n2 = next(it, None)
137
+ if n2 is None: tmp.append((n1, "")); break
138
+ tmp.append((n1, n2))
139
+ movimientos = tmp
140
+ header = (
141
+ '[Event "Partida Reparada"]\n'
142
+ '[White "Blancas"]\n'
143
+ '[Black "Negras"]\n'
144
+ '[Result "*"]\n\n'
145
+ )
146
+ cuerpo = []
147
+ for i, par in enumerate(movimientos, 1):
148
+ b, n = par
149
+ cuerpo.append(f"{i}. {b} {n}".strip())
150
+ return header + " ".join(cuerpo).strip() + ("\n" if cuerpo else "")
151
+
152
+ def _evaluar_posicion(self, board: chess.Board, jugada_num: int, white: str, black: str):
153
+ valores = {'p': 1, 'n': 3, 'b': 3, 'r': 5, 'q': 9, 'k': 0}
154
+ pieces = board.piece_map().values()
155
+ mat_w = sum(valores.get(p.symbol().lower(), 0) for p in pieces if p.color == chess.WHITE)
156
+ mat_b = sum(valores.get(p.symbol().lower(), 0) for p in pieces if p.color == chess.BLACK)
157
+ ventaja = mat_w - mat_b
158
+ movilidad = board.legal_moves.count()
159
+ return {
160
+ "jugada": jugada_num,
161
+ "evaluacion": ventaja + movilidad * 0.1,
162
+ "material_w": mat_w,
163
+ "material_b": mat_b,
164
+ "material_rel": ventaja,
165
+ "mobility": movilidad,
166
+ "stm": 1 if board.turn == chess.WHITE else 0,
167
+ "phase": self._fase_por_material(mat_w + mat_b),
168
+ "white": white,
169
+ "black": black,
170
+ "elo_white": -1,
171
+ "elo_black": -1,
172
+ "elo_gap": 0,
173
+ # campos binarios aproximados (sin SAN aquí)
174
+ "is_capture": 0,
175
+ "is_check_move": 0,
176
+ "is_castling": 0,
177
+ "is_promotion": 0,
178
+ "in_check": int(board.is_check()),
179
+ "ply": jugada_num - 1,
180
+ }
181
+
182
+ @staticmethod
183
+ def _fase_por_material(total):
184
+ if total > 5000: return 0
185
+ if total > 2000: return 1
186
+ return 2
187
+
188
+ def _df_features_for_model(self):
189
+ if not self.datos_jugadas:
190
+ return None
191
+ return pd.DataFrame(self.datos_jugadas)
192
+
193
+ def generar_grafico(self, blancas: str, negras: str):
194
+ if not self.datos_jugadas:
195
+ fig, ax = plt.subplots(figsize=(10, 6))
196
+ ax.text(0.5, 0.5, "PGN no analizable", ha="center", va="center", fontsize=14, transform=ax.transAxes,
197
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral", alpha=0.7))
198
+ ax.set_axis_off()
199
+ return fig
200
+
201
+ jugadas = [d["jugada"] for d in self.datos_jugadas]
202
+ evals = [d["evaluacion"] for d in self.datos_jugadas]
203
+ material = [d["material_rel"] for d in self.datos_jugadas]
204
+
205
+ fig, ax = plt.subplots(figsize=(12, 6))
206
+ ax.plot(jugadas, evals, linewidth=2, label="Ventaja aprox.")
207
+ ax.fill_between(jugadas, evals, alpha=0.20)
208
+ ax.axhline(0, linestyle="--")
209
+ ax.set_title(f"Análisis: {blancas} vs {negras}")
210
+ ax.set_xlabel("Jugada"); ax.set_ylabel("Valor")
211
+ ax.grid(True, alpha=0.3)
212
+
213
+ # Si hay modelo, dibujamos curva de probabilidad de blunder
214
+ if blunder_model is not None and blunder_features is not None:
215
+ df = self._df_features_for_model()
216
+ missing = [c for c in blunder_features if c not in df.columns]
217
+ for m in missing:
218
+ df[m] = 0
219
+ X = df[blunder_features].astype(float).values
220
+ try:
221
+ prob = blunder_model.predict_proba(X)[:, 1]
222
+ ax.plot(jugadas, prob, linewidth=2, label="Prob. blunder (modelo)")
223
+ except Exception as e:
224
+ print("⚠️ No se pudo inferir prob. de blunder:", e)
225
+
226
+ ax.legend(loc="best")
227
+ fig.tight_layout()
228
+ return fig
229
+
230
+ def generar_reporte(self, blancas: str, negras: str, resultado: str, info_extra: str):
231
+ if not self.datos_jugadas:
232
+ return "## PGN no analizable\nCarga otro PGN o revisa el formato."
233
+ jugadas = [d["jugada"] for d in self.datos_jugadas]
234
+ evals = [d["evaluacion"] for d in self.datos_jugadas]
235
+ material = [d["material_rel"] for d in self.datos_jugadas]
236
+ tiene_modelo = (blunder_model is not None and blunder_features is not None)
237
+ nota = "Modelo de blunders activo." if tiene_modelo else "Modelo de blunders no disponible."
238
+ return f"""
239
+ ## Reporte de partida
240
+ **Blancas:** {blancas}
241
+ **Negras:** {negras}
242
+ **Resultado:** {resultado}
243
+
244
+ **Plies analizados:** {len(jugadas)}
245
+ **Ventaja final aprox.:** {evals[-1]:.2f}
246
+ **Diferencia de material final (W-B):** {material[-1]}
247
+
248
+ {info_extra or ""}
249
+
250
+ _{nota}_
251
+ """
252
+
253
+ # ====== UI ======
254
+ analizador = AnalizadorAjedrez()
255
+
256
+ def ui_analizar(pgn_text: str):
257
+ blancas, negras, resultado, info, _ = analizador.analizar_primera_partida_valida(pgn_text)
258
+ fig = analizador.generar_grafico(blancas, negras)
259
+ reporte = analizador.generar_reporte(blancas, negras, resultado, info)
260
+ pgn_reparado = ReparadorPGN.reparar_pgn(pgn_text)
261
+ return fig, reporte, pgn_reparado
262
+
263
+ with gr.Blocks(title="Analizador PGN (ML-ready) — Doctor Linux") as demo:
264
+ gr.Markdown("# Analizador de Partidas PGN\nCarga un PGN (el input no se modifica).")
265
+ with gr.Row():
266
+ pgn_in = gr.Textbox(label="PGN original", lines=18, placeholder="Pega aquí un PGN. Puede contener múltiples partidas.")
267
+ with gr.Row():
268
+ btn = gr.Button("Analizar")
269
+ with gr.Row():
270
+ graf = gr.Plot(label="Gráfico")
271
+ with gr.Row():
272
+ rep = gr.Markdown(label="Reporte")
273
+ with gr.Row():
274
+ reparado_out = gr.Textbox(label="PGN reparado (para revisión; tu texto original no se toca)", lines=12)
275
+ btn.click(fn=ui_analizar, inputs=[pgn_in], outputs=[graf, rep, reparado_out])
276
+
277
+ if __name__ == "__main__":
278
+ demo.launch()
requirements.txt CHANGED
@@ -1,5 +1,9 @@
1
- chess==1.10.0
2
- matplotlib==3.7.1
3
- numpy==1.24.3
4
- gradio==4.19.1
5
- pillow==10.0.0
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ python-chess>=1.999
3
+ matplotlib>=3.8
4
+ numpy>=1.26
5
+ pandas>=2.2
6
+ scikit-learn>=1.3
7
+ tqdm>=4.66
8
+ joblib>=1.4
9
+ pyarrow>=16.0