Spaces:
Sleeping
Sleeping
Commit
·
e530bd7
1
Parent(s):
40821e9
a new way
Browse files
app.py
CHANGED
|
@@ -9,6 +9,8 @@ from collections import Counter, defaultdict
|
|
| 9 |
import os
|
| 10 |
import time
|
| 11 |
import re
|
|
|
|
|
|
|
| 12 |
|
| 13 |
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
|
| 14 |
genai.configure(api_key=GEMINI_API_KEY)
|
|
@@ -16,13 +18,16 @@ model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
|
| 16 |
|
| 17 |
def get_user_games_from_chess_com(username):
|
| 18 |
try:
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
user_url = f"https://api.chess.com/pub/player/{username}"
|
| 22 |
response = requests.get(user_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 23 |
|
| 24 |
if response.status_code != 200:
|
| 25 |
-
return None, f"❌ Foydalanuvchi topilmadi: {username}. Chess.com'da mavjudligini tekshiring."
|
|
|
|
| 26 |
|
| 27 |
archives_url = f"https://api.chess.com/pub/player/{username}/games/archives"
|
| 28 |
response = requests.get(archives_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
|
@@ -80,6 +85,66 @@ def parse_pgn_content(pgn_content):
|
|
| 80 |
break
|
| 81 |
return games
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def detect_opening(game):
|
| 84 |
"""Detect opening from ECO code or Opening header"""
|
| 85 |
opening = game.headers.get("Opening", "")
|
|
@@ -88,23 +153,31 @@ def detect_opening(game):
|
|
| 88 |
if opening:
|
| 89 |
return opening
|
| 90 |
elif eco:
|
| 91 |
-
return
|
| 92 |
else:
|
| 93 |
return "Unknown Opening"
|
| 94 |
|
| 95 |
def analyze_game_detailed(game, username):
|
| 96 |
board = game.board()
|
| 97 |
mistakes = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
move_number = 0
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
| 103 |
|
| 104 |
user_color = None
|
| 105 |
-
if username_lower
|
| 106 |
user_color = chess.WHITE
|
| 107 |
-
elif username_lower
|
| 108 |
user_color = chess.BLACK
|
| 109 |
|
| 110 |
result = game.headers.get("Result", "*")
|
|
@@ -170,68 +243,161 @@ def analyze_game_detailed(game, username):
|
|
| 170 |
|
| 171 |
return {
|
| 172 |
'mistakes': mistakes,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
'opening': opening,
|
| 174 |
'result': result,
|
| 175 |
'user_color': user_color,
|
| 176 |
'user_result': user_result
|
| 177 |
}
|
| 178 |
|
| 179 |
-
def categorize_mistakes(
|
| 180 |
-
|
|
|
|
| 181 |
return []
|
| 182 |
|
| 183 |
-
|
| 184 |
-
for
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
-
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
'mistake': 'Kichik xatolar',
|
| 193 |
-
'hanging_piece': 'Himoyasiz qoldirish',
|
| 194 |
-
'opening_mistake': 'Debyut xatolari',
|
| 195 |
-
'middlegame_mistake': "O'rta o'yin xatolari",
|
| 196 |
-
'endgame_mistake': 'Endshpil xatolari'
|
| 197 |
-
}
|
| 198 |
|
| 199 |
weaknesses = []
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
puzzles = []
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
try:
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
if response.status_code == 200:
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
def create_board_svg(fen, size=400):
|
| 237 |
"""Create SVG representation of chess position"""
|
|
@@ -292,12 +458,15 @@ MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing! Aniq va amaliy maslahatlar bering."""
|
|
| 292 |
return f"AI tahlil hozircha mavjud emas: {str(e)}"
|
| 293 |
|
| 294 |
def analyze_games(username, pgn_file):
|
|
|
|
|
|
|
| 295 |
if username:
|
| 296 |
-
pgn_content, error = get_user_games_from_chess_com(username)
|
| 297 |
if error:
|
| 298 |
return error, "", "", "", None, None, None, None, None
|
|
|
|
| 299 |
elif pgn_file:
|
| 300 |
-
|
| 301 |
pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
|
| 302 |
else:
|
| 303 |
return "❌ Foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
|
|
@@ -307,7 +476,7 @@ def analyze_games(username, pgn_file):
|
|
| 307 |
if not games:
|
| 308 |
return "❌ O'yinlar topilmadi yoki tahlil qilinmadi", "", "", "", None, None, None, None, None
|
| 309 |
|
| 310 |
-
|
| 311 |
opening_stats = defaultdict(lambda: {'wins': 0, 'losses': 0, 'draws': 0, 'total': 0})
|
| 312 |
color_stats = {
|
| 313 |
'white': {'wins': 0, 'losses': 0, 'draws': 0},
|
|
@@ -315,30 +484,30 @@ def analyze_games(username, pgn_file):
|
|
| 315 |
}
|
| 316 |
|
| 317 |
for game in games[:30]:
|
| 318 |
-
analysis = analyze_game_detailed(game,
|
| 319 |
-
|
| 320 |
|
| 321 |
opening = analysis['opening']
|
| 322 |
user_result = analysis.get('user_result')
|
| 323 |
user_color = analysis['user_color']
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
weaknesses = categorize_mistakes(
|
| 342 |
|
| 343 |
# Statistics Report
|
| 344 |
stats_report = f"## 📊 {len(games)} ta o'yin tahlili\n\n"
|
|
@@ -388,8 +557,9 @@ def analyze_games(username, pgn_file):
|
|
| 388 |
ai_analysis = get_comprehensive_analysis(weaknesses, opening_stats, color_stats, len(games))
|
| 389 |
ai_report = f"## 🤖 AI Murabbiy: To'liq Tahlil va O'quv Rejasi\n\n{ai_analysis}"
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
|
|
|
| 393 |
|
| 394 |
# Fix: Format puzzle output as requested
|
| 395 |
puzzle_text = "## 🧩 Sizning shaxsiy masalalaringiz\n\n"
|
|
@@ -416,30 +586,6 @@ def analyze_games(username, pgn_file):
|
|
| 416 |
"",
|
| 417 |
None
|
| 418 |
)
|
| 419 |
-
|
| 420 |
-
puzzle_svgs = []
|
| 421 |
-
puzzle_info = []
|
| 422 |
-
|
| 423 |
-
for i, puzzle in enumerate(puzzles):
|
| 424 |
-
svg = create_board_svg(puzzle['fen'], size=300)
|
| 425 |
-
puzzle_svgs.append(svg)
|
| 426 |
-
|
| 427 |
-
themes = ", ".join(puzzle['themes'][:2]) if 'themes' in puzzle else "Taktika"
|
| 428 |
-
info = f"**Masala {i+1}**: {themes.title()} (Reyting: {puzzle['rating']})"
|
| 429 |
-
puzzle_info.append(info)
|
| 430 |
-
|
| 431 |
-
return (
|
| 432 |
-
full_report,
|
| 433 |
-
ai_report,
|
| 434 |
-
"## 🧩 Sizning shaxsiy masalalaringiz\n\nQuyidagi pozitsiyalarda eng yaxshi yurishni toping:",
|
| 435 |
-
puzzle_info[0] if len(puzzle_info) > 0 else "",
|
| 436 |
-
puzzle_svgs[0] if len(puzzle_svgs) > 0 else None,
|
| 437 |
-
puzzle_info[1] if len(puzzle_info) > 1 else "",
|
| 438 |
-
puzzle_svgs[1] if len(puzzle_svgs) > 1 else None,
|
| 439 |
-
puzzle_info[2] if len(puzzle_info) > 2 else "",
|
| 440 |
-
puzzle_svgs[2] if len(puzzle_svgs) > 2 else None
|
| 441 |
-
)
|
| 442 |
-
|
| 443 |
with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
|
| 444 |
gr.Markdown("""
|
| 445 |
# ♟️ Professional Шахмат O'quv Rejasi
|
|
|
|
| 9 |
import os
|
| 10 |
import time
|
| 11 |
import re
|
| 12 |
+
import chess.polyglot
|
| 13 |
+
|
| 14 |
|
| 15 |
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
|
| 16 |
genai.configure(api_key=GEMINI_API_KEY)
|
|
|
|
| 18 |
|
| 19 |
def get_user_games_from_chess_com(username):
|
| 20 |
try:
|
| 21 |
+
# Keep original case but normalize for API
|
| 22 |
+
username_original = username.strip()
|
| 23 |
+
username = username_original.lower()
|
| 24 |
|
| 25 |
user_url = f"https://api.chess.com/pub/player/{username}"
|
| 26 |
response = requests.get(user_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 27 |
|
| 28 |
if response.status_code != 200:
|
| 29 |
+
return None, None, f"❌ Foydalanuvchi topilmadi: {username}. Chess.com'da mavjudligini tekshiring."
|
| 30 |
+
|
| 31 |
|
| 32 |
archives_url = f"https://api.chess.com/pub/player/{username}/games/archives"
|
| 33 |
response = requests.get(archives_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
|
|
|
| 85 |
break
|
| 86 |
return games
|
| 87 |
|
| 88 |
+
def get_opening_name_from_eco(eco_code):
|
| 89 |
+
"""Convert ECO code to opening name using chess library"""
|
| 90 |
+
eco_openings = {
|
| 91 |
+
'A00': 'Uncommon Opening',
|
| 92 |
+
'A01': "Nimzowitsch-Larsen Attack",
|
| 93 |
+
'A02': "Bird's Opening",
|
| 94 |
+
'A03': "Bird's Opening: Dutch Variation",
|
| 95 |
+
'A04': 'Reti Opening',
|
| 96 |
+
'A05': 'Reti Opening: 1...Nf6',
|
| 97 |
+
'A06': 'Reti Opening: 2.b3',
|
| 98 |
+
'A10': 'English Opening',
|
| 99 |
+
'A15': 'English Opening: Anglo-Indian Defense',
|
| 100 |
+
'A20': 'English Opening: 1...e5',
|
| 101 |
+
'A30': 'English Opening: Symmetrical Variation',
|
| 102 |
+
'A40': 'Queen Pawn Game',
|
| 103 |
+
'A45': 'Indian Defense',
|
| 104 |
+
'A46': 'Indian Defense: 2.Nf3',
|
| 105 |
+
'A50': 'Indian Defense: Normal Variation',
|
| 106 |
+
'B00': 'Uncommon King Pawn Opening',
|
| 107 |
+
'B01': 'Scandinavian Defense',
|
| 108 |
+
'B02': "Alekhine's Defense",
|
| 109 |
+
'B03': "Alekhine's Defense: Four Pawns Attack",
|
| 110 |
+
'B10': 'Caro-Kann Defense',
|
| 111 |
+
'B12': 'Caro-Kann Defense: Advance Variation',
|
| 112 |
+
'B20': 'Sicilian Defense',
|
| 113 |
+
'B22': 'Sicilian Defense: Alapin Variation',
|
| 114 |
+
'B23': 'Sicilian Defense: Closed',
|
| 115 |
+
'B30': 'Sicilian Defense: 2...Nc6',
|
| 116 |
+
'B40': 'Sicilian Defense: French Variation',
|
| 117 |
+
'B50': 'Sicilian Defense: 2...d6',
|
| 118 |
+
'B90': 'Sicilian Defense: Najdorf',
|
| 119 |
+
'C00': 'French Defense',
|
| 120 |
+
'C02': 'French Defense: Advance Variation',
|
| 121 |
+
'C10': 'French Defense: Rubinstein Variation',
|
| 122 |
+
'C20': 'King Pawn Game',
|
| 123 |
+
'C30': "King's Gambit",
|
| 124 |
+
'C40': "King's Knight Opening",
|
| 125 |
+
'C41': 'Philidor Defense',
|
| 126 |
+
'C42': 'Russian Game (Petrov Defense)',
|
| 127 |
+
'C44': 'Scotch Game',
|
| 128 |
+
'C50': 'Italian Game',
|
| 129 |
+
'C60': 'Spanish Opening (Ruy Lopez)',
|
| 130 |
+
'C65': 'Spanish Opening: Berlin Defense',
|
| 131 |
+
'D00': 'Queen Pawn Game',
|
| 132 |
+
'D02': 'Queen Pawn Game: 2.Nf3',
|
| 133 |
+
'D10': 'Slav Defense',
|
| 134 |
+
'D20': "Queen's Gambit Accepted",
|
| 135 |
+
'D30': "Queen's Gambit Declined",
|
| 136 |
+
'D50': "Queen's Gambit Declined: 4.Bg5",
|
| 137 |
+
'E00': 'Indian Defense',
|
| 138 |
+
'E10': 'Indian Defense: 3.Nf3',
|
| 139 |
+
'E20': 'Nimzo-Indian Defense',
|
| 140 |
+
'E30': 'Nimzo-Indian Defense: Leningrad Variation',
|
| 141 |
+
'E60': "King's Indian Defense",
|
| 142 |
+
'E70': "King's Indian Defense: Normal Variation",
|
| 143 |
+
'E90': "King's Indian Defense: Orthodox Variation",
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
return eco_openings.get(eco_code, f"ECO {eco_code}")
|
| 147 |
+
|
| 148 |
def detect_opening(game):
|
| 149 |
"""Detect opening from ECO code or Opening header"""
|
| 150 |
opening = game.headers.get("Opening", "")
|
|
|
|
| 153 |
if opening:
|
| 154 |
return opening
|
| 155 |
elif eco:
|
| 156 |
+
return get_opening_name_from_eco(eco)
|
| 157 |
else:
|
| 158 |
return "Unknown Opening"
|
| 159 |
|
| 160 |
def analyze_game_detailed(game, username):
|
| 161 |
board = game.board()
|
| 162 |
mistakes = []
|
| 163 |
+
blunders = []
|
| 164 |
+
mistake_moves = []
|
| 165 |
+
hanging_pieces = []
|
| 166 |
+
opening_mistakes = []
|
| 167 |
+
middlegame_mistakes = []
|
| 168 |
+
endgame_mistakes = []
|
| 169 |
+
|
| 170 |
move_number = 0
|
| 171 |
|
| 172 |
+
# Normalize all player names to lowercase
|
| 173 |
+
white_player = game.headers.get("White", "").strip().lower()
|
| 174 |
+
black_player = game.headers.get("Black", "").strip().lower()
|
| 175 |
+
username_lower = username.strip().lower()
|
| 176 |
|
| 177 |
user_color = None
|
| 178 |
+
if username_lower == white_player:
|
| 179 |
user_color = chess.WHITE
|
| 180 |
+
elif username_lower == black_player:
|
| 181 |
user_color = chess.BLACK
|
| 182 |
|
| 183 |
result = game.headers.get("Result", "*")
|
|
|
|
| 243 |
|
| 244 |
return {
|
| 245 |
'mistakes': mistakes,
|
| 246 |
+
'blunders': blunders,
|
| 247 |
+
'mistake_moves': mistake_moves,
|
| 248 |
+
'hanging_pieces': hanging_pieces,
|
| 249 |
+
'opening_mistakes': opening_mistakes,
|
| 250 |
+
'middlegame_mistakes': middlegame_mistakes,
|
| 251 |
+
'endgame_mistakes': endgame_mistakes,
|
| 252 |
'opening': opening,
|
| 253 |
'result': result,
|
| 254 |
'user_color': user_color,
|
| 255 |
'user_result': user_result
|
| 256 |
}
|
| 257 |
|
| 258 |
+
def categorize_mistakes(all_analyses):
|
| 259 |
+
"""Categorize all types of mistakes with proper counting"""
|
| 260 |
+
if not all_analyses:
|
| 261 |
return []
|
| 262 |
|
| 263 |
+
# Count all mistake types
|
| 264 |
+
blunders = sum(len(a['blunders']) for a in all_analyses)
|
| 265 |
+
mistakes = sum(len(a['mistake_moves']) for a in all_analyses)
|
| 266 |
+
hanging = sum(len(a['hanging_pieces']) for a in all_analyses)
|
| 267 |
+
opening = sum(len(a['opening_mistakes']) for a in all_analyses)
|
| 268 |
+
middlegame = sum(len(a['middlegame_mistakes']) for a in all_analyses)
|
| 269 |
+
endgame = sum(len(a['endgame_mistakes']) for a in all_analyses)
|
| 270 |
|
| 271 |
+
total = blunders + mistakes + hanging + opening + middlegame + endgame
|
| 272 |
|
| 273 |
+
if total == 0:
|
| 274 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
weaknesses = []
|
| 277 |
+
|
| 278 |
+
if blunders > 0:
|
| 279 |
+
weaknesses.append({
|
| 280 |
+
'category': "Qo'pol xatolar",
|
| 281 |
+
'count': blunders,
|
| 282 |
+
'percentage': (blunders / total * 100)
|
| 283 |
+
})
|
| 284 |
+
|
| 285 |
+
if mistakes > 0:
|
| 286 |
+
weaknesses.append({
|
| 287 |
+
'category': 'Kichik xatolar',
|
| 288 |
+
'count': mistakes,
|
| 289 |
+
'percentage': (mistakes / total * 100)
|
| 290 |
+
})
|
| 291 |
+
|
| 292 |
+
if hanging > 0:
|
| 293 |
+
weaknesses.append({
|
| 294 |
+
'category': 'Himoyasiz qoldirish',
|
| 295 |
+
'count': hanging,
|
| 296 |
+
'percentage': (hanging / total * 100)
|
| 297 |
+
})
|
| 298 |
+
|
| 299 |
+
if opening > 0:
|
| 300 |
+
weaknesses.append({
|
| 301 |
+
'category': 'Debyut xatolari',
|
| 302 |
+
'count': opening,
|
| 303 |
+
'percentage': (opening / total * 100)
|
| 304 |
+
})
|
| 305 |
+
|
| 306 |
+
if middlegame > 0:
|
| 307 |
+
weaknesses.append({
|
| 308 |
+
'category': "O'rta o'yin xatolari",
|
| 309 |
+
'count': middlegame,
|
| 310 |
+
'percentage': (middlegame / total * 100)
|
| 311 |
+
})
|
| 312 |
+
|
| 313 |
+
if endgame > 0:
|
| 314 |
+
weaknesses.append({
|
| 315 |
+
'category': 'Endshpil xatolari',
|
| 316 |
+
'count': endgame,
|
| 317 |
+
'percentage': (endgame / total * 100)
|
| 318 |
+
})
|
| 319 |
+
|
| 320 |
+
# Sort by count descending
|
| 321 |
+
weaknesses.sort(key=lambda x: x['count'], reverse=True)
|
| 322 |
+
|
| 323 |
+
return weaknesses
|
| 324 |
+
def fetch_lichess_puzzles(themes, count=5):
|
| 325 |
+
"""Fetch puzzles from Lichess based on detected weakness themes"""
|
| 326 |
puzzles = []
|
| 327 |
|
| 328 |
+
# Map weakness categories to Lichess puzzle themes
|
| 329 |
+
theme_map = {
|
| 330 |
+
"Qo'pol xatolar": ['blunder', 'hangingPiece', 'sacrifice'],
|
| 331 |
+
'Kichik xatolar': ['advantage', 'endgame', 'middlegame'],
|
| 332 |
+
'Himoyasiz qoldirish': ['hangingPiece', 'attraction', 'exposedKing'],
|
| 333 |
+
'Debyut xatolari': ['opening', 'short'],
|
| 334 |
+
"O'rta o'yin xatolari": ['middlegame', 'attackingF2F7', 'advancedPawn'],
|
| 335 |
+
'Endshpil xatolari': ['endgame', 'queenEndgame', 'rookEndgame']
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
# Collect all relevant themes
|
| 339 |
+
lichess_themes = []
|
| 340 |
+
for theme_name in themes:
|
| 341 |
+
if theme_name in theme_map:
|
| 342 |
+
lichess_themes.extend(theme_map[theme_name])
|
| 343 |
+
|
| 344 |
+
# Remove duplicates
|
| 345 |
+
lichess_themes = list(set(lichess_themes))[:3]
|
| 346 |
+
|
| 347 |
try:
|
| 348 |
+
# Use batch API to get multiple puzzles
|
| 349 |
+
angle = ','.join(lichess_themes) if lichess_themes else 'mix'
|
| 350 |
+
url = f"https://lichess.org/api/puzzle/batch/{angle}"
|
| 351 |
+
|
| 352 |
+
response = requests.get(url, timeout=15, headers={
|
| 353 |
+
'User-Agent': 'Mozilla/5.0',
|
| 354 |
+
'Accept': 'application/x-ndjson'
|
| 355 |
+
})
|
| 356 |
|
| 357 |
if response.status_code == 200:
|
| 358 |
+
# Parse NDJSON response
|
| 359 |
+
lines = response.text.strip().split('\n')
|
| 360 |
+
for line in lines[:count]:
|
| 361 |
+
try:
|
| 362 |
+
puzzle_data = eval(line) # or use json.loads(line)
|
| 363 |
+
puzzle_game = puzzle_data['game']
|
| 364 |
+
puzzle_info = puzzle_data['puzzle']
|
| 365 |
+
|
| 366 |
+
puzzles.append({
|
| 367 |
+
'id': puzzle_info['id'],
|
| 368 |
+
'fen': puzzle_game.get('fen', ''),
|
| 369 |
+
'moves': puzzle_game.get('pgn', '').split()[-1],
|
| 370 |
+
'solution': puzzle_info.get('solution', []),
|
| 371 |
+
'rating': puzzle_info.get('rating', 1500),
|
| 372 |
+
'themes': puzzle_info.get('themes', []),
|
| 373 |
+
'url': f"https://lichess.org/training/{puzzle_info['id']}"
|
| 374 |
+
})
|
| 375 |
+
except:
|
| 376 |
+
continue
|
| 377 |
+
except Exception as e:
|
| 378 |
+
print(f"Error fetching puzzles: {e}")
|
| 379 |
+
|
| 380 |
+
# Fallback if not enough puzzles
|
| 381 |
+
if len(puzzles) < count:
|
| 382 |
+
try:
|
| 383 |
+
# Try daily puzzle as backup
|
| 384 |
+
url = "https://lichess.org/api/puzzle/daily"
|
| 385 |
+
response = requests.get(url, timeout=10)
|
| 386 |
+
if response.status_code == 200:
|
| 387 |
+
daily = response.json()
|
| 388 |
+
puzzles.append({
|
| 389 |
+
'id': daily['puzzle']['id'],
|
| 390 |
+
'fen': daily['game'].get('fen', ''),
|
| 391 |
+
'moves': daily['game'].get('pgn', '').split()[-1],
|
| 392 |
+
'solution': daily['puzzle'].get('solution', []),
|
| 393 |
+
'rating': daily['puzzle'].get('rating', 1500),
|
| 394 |
+
'themes': daily['puzzle'].get('themes', []),
|
| 395 |
+
'url': f"https://lichess.org/training/{daily['puzzle']['id']}"
|
| 396 |
+
})
|
| 397 |
+
except:
|
| 398 |
+
pass
|
| 399 |
+
|
| 400 |
+
return puzzles[:count]
|
| 401 |
|
| 402 |
def create_board_svg(fen, size=400):
|
| 403 |
"""Create SVG representation of chess position"""
|
|
|
|
| 458 |
return f"AI tahlil hozircha mavjud emas: {str(e)}"
|
| 459 |
|
| 460 |
def analyze_games(username, pgn_file):
|
| 461 |
+
actual_username = username # Store for later use
|
| 462 |
+
|
| 463 |
if username:
|
| 464 |
+
pgn_content, returned_username, error = get_user_games_from_chess_com(username)
|
| 465 |
if error:
|
| 466 |
return error, "", "", "", None, None, None, None, None
|
| 467 |
+
actual_username = returned_username
|
| 468 |
elif pgn_file:
|
| 469 |
+
actual_username = "Player"
|
| 470 |
pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
|
| 471 |
else:
|
| 472 |
return "❌ Foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
|
|
|
|
| 476 |
if not games:
|
| 477 |
return "❌ O'yinlar topilmadi yoki tahlil qilinmadi", "", "", "", None, None, None, None, None
|
| 478 |
|
| 479 |
+
all_analyses = []
|
| 480 |
opening_stats = defaultdict(lambda: {'wins': 0, 'losses': 0, 'draws': 0, 'total': 0})
|
| 481 |
color_stats = {
|
| 482 |
'white': {'wins': 0, 'losses': 0, 'draws': 0},
|
|
|
|
| 484 |
}
|
| 485 |
|
| 486 |
for game in games[:30]:
|
| 487 |
+
analysis = analyze_game_detailed(game, actual_username)
|
| 488 |
+
all_analyses.append(analysis)
|
| 489 |
|
| 490 |
opening = analysis['opening']
|
| 491 |
user_result = analysis.get('user_result')
|
| 492 |
user_color = analysis['user_color']
|
| 493 |
|
| 494 |
+
if user_color is not None:
|
| 495 |
+
opening_stats[opening]['total'] += 1
|
| 496 |
+
|
| 497 |
+
if user_result == 'win':
|
| 498 |
+
opening_stats[opening]['wins'] += 1
|
| 499 |
+
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 500 |
+
color_stats[color_key]['wins'] += 1
|
| 501 |
+
elif user_result == 'loss':
|
| 502 |
+
opening_stats[opening]['losses'] += 1
|
| 503 |
+
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 504 |
+
color_stats[color_key]['losses'] += 1
|
| 505 |
+
elif user_result == 'draw':
|
| 506 |
+
opening_stats[opening]['draws'] += 1
|
| 507 |
+
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 508 |
+
color_stats[color_key]['draws'] += 1
|
| 509 |
+
|
| 510 |
+
weaknesses = categorize_mistakes(all_analyses)
|
| 511 |
|
| 512 |
# Statistics Report
|
| 513 |
stats_report = f"## 📊 {len(games)} ta o'yin tahlili\n\n"
|
|
|
|
| 557 |
ai_analysis = get_comprehensive_analysis(weaknesses, opening_stats, color_stats, len(games))
|
| 558 |
ai_report = f"## 🤖 AI Murabbiy: To'liq Tahlil va O'quv Rejasi\n\n{ai_analysis}"
|
| 559 |
|
| 560 |
+
# Get puzzles based on top 3 weaknesses
|
| 561 |
+
weakness_themes = [w['category'] for w in weaknesses[:3]]
|
| 562 |
+
puzzles = fetch_lichess_puzzles(weakness_themes, count=5)
|
| 563 |
|
| 564 |
# Fix: Format puzzle output as requested
|
| 565 |
puzzle_text = "## 🧩 Sizning shaxsiy masalalaringiz\n\n"
|
|
|
|
| 586 |
"",
|
| 587 |
None
|
| 588 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
|
| 590 |
gr.Markdown("""
|
| 591 |
# ♟️ Professional Шахмат O'quv Rejasi
|