ffzeroHua commited on
Commit
891730d
·
verified ·
1 Parent(s): 4f88a83

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -52
app.py CHANGED
@@ -5,45 +5,50 @@ import random
5
  import torch
6
  import threading
7
  import time
 
 
8
  import gradio as gr
9
  import pandas as pd
10
  import matplotlib.pyplot as plt
11
- import shutil
12
- from huggingface_hub import hf_hub_download, HfApi
13
 
14
  from riichienv import RiichiEnv, GameRule
15
  from model3pLOCAL import load_model
16
 
17
  # ==========================================
18
- # 0. 云端持久化与全局配置
19
  # ==========================================
20
  REPO_ID = "ffzeroHua/mj-eval-results" # ⚠️ 请务必替换为你在 HF 创建的 Dataset 名称
21
- REPORT_FILE = "eval_report.txt"
22
  HF_TOKEN = os.getenv("HF_TOKEN")
23
- api = HfApi()
24
 
 
 
 
 
 
 
25
  EVAL_RUNNING = True
26
 
27
  def sync_from_hub():
28
- """启动时从数据集下载有的战绩文件"""
29
  if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
30
  try:
31
- print(f"🔄 正在从 Hub 下载战绩文件...")
32
- path = hf_hub_download(
33
  repo_id=REPO_ID,
34
- filename=REPORT_FILE,
35
  repo_type="dataset",
 
 
36
  token=HF_TOKEN
37
  )
38
- shutil.copy(path, REPORT_FILE)
39
- print("✅ 下载完成,已恢复历史数据。")
40
  except Exception as e:
41
- print(f"⚠️ 未能下载历史战绩 (可能是首次运行或文件不存在): {e}")
42
  else:
43
- print("⚠️ 未配置 HF_TOKEN 或 REPO_ID,跳过云端拉取。")
44
 
45
  def sync_to_hub():
46
- """将本地战绩文件备份到数据集"""
47
  if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
48
  try:
49
  api.upload_file(
@@ -53,7 +58,7 @@ def sync_to_hub():
53
  repo_type="dataset",
54
  token=HF_TOKEN
55
  )
56
- print(f"☁️ 战绩已成功同步至 Hub: {time.strftime('%H:%M:%S')}")
57
  except Exception as e:
58
  print(f"❌ 同步失败: {e}")
59
 
@@ -134,11 +139,12 @@ def play_one_game(game_index):
134
  # 3. 后台独立评估线程
135
  # ==========================================
136
  def background_eval_loop():
137
- sync_from_hub() # 启动时拉取历史数据
138
 
139
  NUM_WORKERS = 1 # Docker/云端免费 CPU 推荐设为 1
140
- print("🚀 后台对战线程已启动...")
141
 
 
142
  if not os.path.exists(REPORT_FILE):
143
  open(REPORT_FILE, 'w').close()
144
 
@@ -161,42 +167,44 @@ def background_eval_loop():
161
  f.flush()
162
  games_completed += 1
163
  games_since_last_sync += 1
164
- print(f"完成 {games_completed} 局: 顺位 {rank}, 得点 {score}")
165
  except Exception as e:
166
  print(f"对局异常: {e}")
167
 
168
  if EVAL_RUNNING:
169
  futures.add(executor.submit(play_one_game, games_completed))
170
 
171
- # 每完成 5 局,自动向云端备份一次
172
  if games_since_last_sync >= 5:
173
  sync_to_hub()
174
  games_since_last_sync = 0
175
 
176
  # ==========================================
177
- # 4. 前端 Gradio 实时展示面板
178
  # ==========================================
179
  def read_and_analyze():
180
- """读取战绩文件,并生成统计文本和图表"""
181
- if not os.path.exists(REPORT_FILE):
 
182
  return "⏳ 等待模型完成第一局对战...", None
183
 
 
184
  try:
185
- with open(REPORT_FILE, "r") as f:
186
- lines = f.readlines()
187
-
188
- ranks, scores = [], []
189
- for line in lines:
190
- parts = line.strip().split()
191
- if len(parts) == 2:
192
- ranks.append(int(float(parts[0])))
193
- scores.append(float(parts[1]))
194
-
195
  total = len(ranks)
196
  if total == 0:
197
  return "⏳ 正在进行第一局对战...", None
198
 
199
- # 计算数据
200
  avg_rank = sum(ranks) / total
201
  avg_score = sum(scores) / total
202
  rank1_rate = ranks.count(1) / total * 100
@@ -207,16 +215,17 @@ def read_and_analyze():
207
 
208
  # 格式化 Markdown 文本
209
  md_text = f"""
210
- ### 📊 实时对抗统计 (新模型 1v2 旧模型)
211
- - **总对局数:** {total} 局
212
- - **平均顺位:** {avg_rank:.3f}
213
- - **平均得点:** {avg_score:.0f}
214
  ---
215
  - 🥇 **一位率:** {rank1_rate:.1f}%
216
  - 🥈 **二位率:** {rank2_rate:.1f}%
217
  - 🥉 **三位率:** {rank3_rate:.1f}%
218
  ---
219
- - 🕒 **最后更新/同步时间:** {last_update}
 
220
  """
221
 
222
  # 绘制图表
@@ -224,7 +233,7 @@ def read_and_analyze():
224
 
225
  ax1 = fig.add_subplot(121)
226
  ax1.bar(['1st', '2nd', '3rd'], [rank1_rate, rank2_rate, rank3_rate], color=['#FFD700', '#C0C0C0', '#CD7F32'])
227
- ax1.set_title('Rank Distribution (%)')
228
  ax1.set_ylim(0, max(100, max([rank1_rate, rank2_rate, rank3_rate] + [0]) + 10))
229
  for i, v in enumerate([rank1_rate, rank2_rate, rank3_rate]):
230
  ax1.text(i, v + 2, f"{v:.1f}%", ha='center')
@@ -234,7 +243,7 @@ def read_and_analyze():
234
  df['ma'] = df['score'].rolling(window=min(10, max(1, len(df))), min_periods=1).mean()
235
  ax2.plot(df['score'], alpha=0.3, color='gray', label='Raw Score')
236
  ax2.plot(df['ma'], color='blue', linewidth=2, label='Moving Avg (10)')
237
- ax2.set_title('Score Trend')
238
  ax2.legend()
239
 
240
  plt.tight_layout()
@@ -245,27 +254,23 @@ def read_and_analyze():
245
  return f"❌ 数据解析出错: {e}", None
246
 
247
  # ==========================================
248
- # 5. 启动 Gradio 应用
249
- # ==========================================
250
- # ==========================================
251
- # 5. 启动 Gradio 应用
252
  # ==========================================
253
- # 修复 1: 移除这里的 theme 参数
254
  with gr.Blocks() as demo:
255
- gr.Markdown("# 🀄 Mahjong AI 实时强度评估舱")
256
- gr.Markdown("新模型正在后台随机座次与两名旧模型进行战斗。页面将自动刷新展示最新战绩。战绩会自动同步至 Hugging Face Dataset。")
257
 
258
  with gr.Row():
259
  with gr.Column(scale=1):
260
  stats_output = gr.Markdown("🚀 正在启动后台环境...")
261
- refresh_btn = gr.Button("🔄 手动刷新战绩")
262
  with gr.Column(scale=2):
263
  plot_output = gr.Plot()
264
 
265
- # 修复 2: 使用 demo.load 仅处理用户刚打开页面时的第一次渲染
266
  demo.load(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
267
 
268
- # 修复 3: 使用新版 gr.Timer 来替代 every=15 实现定时刷新
269
  timer = gr.Timer(15)
270
  timer.tick(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
271
 
@@ -276,6 +281,5 @@ if __name__ == "__main__":
276
  t = threading.Thread(target=background_eval_loop, daemon=True)
277
  t.start()
278
 
279
- # 2. 启动前端网页
280
- # 修复 4: 将 theme 参数转移到 launch() 里面
281
  demo.queue().launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())
 
5
  import torch
6
  import threading
7
  import time
8
+ import uuid
9
+ import glob
10
  import gradio as gr
11
  import pandas as pd
12
  import matplotlib.pyplot as plt
13
+ from huggingface_hub import snapshot_download, HfApi
 
14
 
15
  from riichienv import RiichiEnv, GameRule
16
  from model3pLOCAL import load_model
17
 
18
  # ==========================================
19
+ # 0. 分布式多开与云端持久化配置
20
  # ==========================================
21
  REPO_ID = "ffzeroHua/mj-eval-results" # ⚠️ 请务必替换为你在 HF 创建的 Dataset 名称
 
22
  HF_TOKEN = os.getenv("HF_TOKEN")
 
23
 
24
+ # 为当前 Space 节点生成唯一的 ID (如果在本地跑,也会生成唯一 ID)
25
+ WORKER_ID = os.getenv("WORKER_ID", str(uuid.uuid4())[:6])
26
+ REPORT_FILE_PREFIX = 'eval_report'
27
+ REPORT_FILE = f"{REPORT_FILE_PREFIX}_{WORKER_ID}.txt"
28
+
29
+ api = HfApi()
30
  EVAL_RUNNING = True
31
 
32
  def sync_from_hub():
33
+ """启动时从数据集下载节点的战绩分片文件"""
34
  if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
35
  try:
36
+ print(f"🔄 正在从 Hub 拉取全局历史数据...")
37
+ snapshot_download(
38
  repo_id=REPO_ID,
 
39
  repo_type="dataset",
40
+ local_dir=".",
41
+ allow_patterns=REPORT_FILE_PREFIX + "_*.txt", # 只下载战绩文件
42
  token=HF_TOKEN
43
  )
44
+ print("✅ 历史数据拉取完成。")
 
45
  except Exception as e:
46
+ print(f"⚠️ 拉取历史战绩失败 (可能是首次运行或尚无数据): {e}")
47
  else:
48
+ print("⚠️ 未配置 HF_TOKEN 或 REPO_ID 未修改,跳过云端拉取。")
49
 
50
  def sync_to_hub():
51
+ """将当前节点的战绩文件备份到数据集"""
52
  if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
53
  try:
54
  api.upload_file(
 
58
  repo_type="dataset",
59
  token=HF_TOKEN
60
  )
61
+ print(f"☁️ 节点 {WORKER_ID} 战绩已成功同步至 Hub: {time.strftime('%H:%M:%S')}")
62
  except Exception as e:
63
  print(f"❌ 同步失败: {e}")
64
 
 
139
  # 3. 后台独立评估线程
140
  # ==========================================
141
  def background_eval_loop():
142
+ sync_from_hub() # 启动时拉取全局历史数据
143
 
144
  NUM_WORKERS = 1 # Docker/云端免费 CPU 推荐设为 1
145
+ print(f"🚀 节点 [{WORKER_ID}] 后台对战线程已启动...")
146
 
147
+ # 确保当前节点的文件存在
148
  if not os.path.exists(REPORT_FILE):
149
  open(REPORT_FILE, 'w').close()
150
 
 
167
  f.flush()
168
  games_completed += 1
169
  games_since_last_sync += 1
170
+ print(f"[节点 {WORKER_ID}] 完成 {games_completed} 局: 顺位 {rank}, 得点 {score}")
171
  except Exception as e:
172
  print(f"对局异常: {e}")
173
 
174
  if EVAL_RUNNING:
175
  futures.add(executor.submit(play_one_game, games_completed))
176
 
177
+ # 每完成 5 局,自动向云端备份一次当前节点的文件
178
  if games_since_last_sync >= 5:
179
  sync_to_hub()
180
  games_since_last_sync = 0
181
 
182
  # ==========================================
183
+ # 4. 前端 Gradio 实时展示面板 (全局汇总)
184
  # ==========================================
185
  def read_and_analyze():
186
+ """读取所有节点的战绩文件,并生成全局汇总统计文本和图表"""
187
+ all_files = glob.glob(f"{REPORT_FILE_PREFIX}_*.txt")
188
+ if not all_files:
189
  return "⏳ 等待模型完成第一局对战...", None
190
 
191
+ ranks, scores = [], []
192
  try:
193
+ # 遍历汇总所有节点数据
194
+ for file in all_files:
195
+ with open(file, "r") as f:
196
+ lines = f.readlines()
197
+ for line in lines:
198
+ parts = line.strip().split()
199
+ if len(parts) == 2:
200
+ ranks.append(int(float(parts[0])))
201
+ scores.append(float(parts[1]))
202
+
203
  total = len(ranks)
204
  if total == 0:
205
  return "⏳ 正在进行第一局对战...", None
206
 
207
+ # 计算全局数据
208
  avg_rank = sum(ranks) / total
209
  avg_score = sum(scores) / total
210
  rank1_rate = ranks.count(1) / total * 100
 
215
 
216
  # 格式化 Markdown 文本
217
  md_text = f"""
218
+ ### 📊 分布式集群全局对抗统计 (新模型 1v2 旧模型)
219
+ - **总汇集对局数:** {total} 局 (来自 {len(all_files)} 个活跃/历史节点)
220
+ - **全局平均顺位:** {avg_rank:.3f}
221
+ - **全局平均得点:** {avg_score:.0f}
222
  ---
223
  - 🥇 **一位率:** {rank1_rate:.1f}%
224
  - 🥈 **二位率:** {rank2_rate:.1f}%
225
  - 🥉 **三位率:** {rank3_rate:.1f}%
226
  ---
227
+ - 🌐 **当前节点 ID:** `{WORKER_ID}`
228
+ - 🕒 **页面刷新时间:** {last_update}
229
  """
230
 
231
  # 绘制图表
 
233
 
234
  ax1 = fig.add_subplot(121)
235
  ax1.bar(['1st', '2nd', '3rd'], [rank1_rate, rank2_rate, rank3_rate], color=['#FFD700', '#C0C0C0', '#CD7F32'])
236
+ ax1.set_title('Global Rank Distribution (%)')
237
  ax1.set_ylim(0, max(100, max([rank1_rate, rank2_rate, rank3_rate] + [0]) + 10))
238
  for i, v in enumerate([rank1_rate, rank2_rate, rank3_rate]):
239
  ax1.text(i, v + 2, f"{v:.1f}%", ha='center')
 
243
  df['ma'] = df['score'].rolling(window=min(10, max(1, len(df))), min_periods=1).mean()
244
  ax2.plot(df['score'], alpha=0.3, color='gray', label='Raw Score')
245
  ax2.plot(df['ma'], color='blue', linewidth=2, label='Moving Avg (10)')
246
+ ax2.set_title('Global Score Trend')
247
  ax2.legend()
248
 
249
  plt.tight_layout()
 
254
  return f"❌ 数据解析出错: {e}", None
255
 
256
  # ==========================================
257
+ # 5. 启动 Gradio 应用 (兼容 Gradio 6.0+)
 
 
 
258
  # ==========================================
 
259
  with gr.Blocks() as demo:
260
+ gr.Markdown("# 🀄 Mahjong AI 分布式强度评估舱")
261
+ gr.Markdown("新模型正在后台随机座次与两名旧模型进行战斗。战绩会自动通过数据分片同步至 Hugging Face Dataset,页面展示为集群全局汇总数据。")
262
 
263
  with gr.Row():
264
  with gr.Column(scale=1):
265
  stats_output = gr.Markdown("🚀 正在启动后台环境...")
266
+ refresh_btn = gr.Button("🔄 手动刷新全局战绩")
267
  with gr.Column(scale=2):
268
  plot_output = gr.Plot()
269
 
270
+ # 加载渲染
271
  demo.load(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
272
 
273
+ # 兼容新版 Gradio 的定时器 (每 15 刷新一次前端)
274
  timer = gr.Timer(15)
275
  timer.tick(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
276
 
 
281
  t = threading.Thread(target=background_eval_loop, daemon=True)
282
  t.start()
283
 
284
+ # 2. 启动前端网页 (主题参数移至此处)
 
285
  demo.queue().launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())