Update models_manager_v8.4.py
Browse files- models_manager_v8.4.py +159 -30
models_manager_v8.4.py
CHANGED
|
@@ -317,6 +317,9 @@ class ModelsManager(tk.Tk):
|
|
| 317 |
# 3. 管理全局 KV
|
| 318 |
ttk.Button(top, text="🔧 管理全局 KV", command=self.open_fields_manager).pack(side="left", padx=6)
|
| 319 |
|
|
|
|
|
|
|
|
|
|
| 320 |
# 4. 加载本地
|
| 321 |
ttk.Button(top, text="📂 加载本地 models.json", command=self.load_local).pack(side="left", padx=6)
|
| 322 |
|
|
@@ -471,6 +474,25 @@ class ModelsManager(tk.Tk):
|
|
| 471 |
|
| 472 |
ttk.Label(win, text="注意:操作会直接修改内存中的数据,完成后再去点【保存本地】", foreground="red").pack(pady=10)
|
| 473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
def open_dataset_repo(self):
|
| 475 |
import webbrowser
|
| 476 |
webbrowser.open(f"https://huggingface.co/datasets/{DATASET_REPO_ID}/tree/main")
|
|
@@ -626,7 +648,17 @@ class ModelsManager(tk.Tk):
|
|
| 626 |
meta.get("showcase", "")
|
| 627 |
]
|
| 628 |
for col in dynamic_cols:
|
| 629 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
|
| 631 |
self.tree.insert("", "end", iid=path, text=path, values=row_values, tags=tags)
|
| 632 |
|
|
@@ -747,19 +779,32 @@ class ModelsManager(tk.Tk):
|
|
| 747 |
|
| 748 |
# 🌟 核心修改:深拷贝一份用于上传
|
| 749 |
upload_items = copy.deepcopy(sorted_items)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
|
| 751 |
for path, meta in upload_items.items():
|
| 752 |
if not isinstance(meta, dict): continue
|
| 753 |
|
| 754 |
-
|
| 755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
|
| 757 |
if meta.get("showcase"):
|
| 758 |
-
prefix = f"{MODEL_BASE}/tree/main/{SHOWCASE_DIR}
|
| 759 |
-
|
|
|
|
|
|
|
| 760 |
|
| 761 |
-
if meta.get("guide"):
|
| 762 |
-
|
|
|
|
| 763 |
|
| 764 |
tmp = TEMP_UPLOAD_JSON
|
| 765 |
with open(tmp, "w", encoding="utf-8") as f:
|
|
@@ -1150,15 +1195,13 @@ class ModelsManager(tk.Tk):
|
|
| 1150 |
try:
|
| 1151 |
from huggingface_hub import CommitOperationAdd
|
| 1152 |
|
| 1153 |
-
# 🌟 开始上传,禁用按钮,重置进度条
|
| 1154 |
self.after(0, lambda: self.btn_upload.configure(state="disabled"))
|
| 1155 |
self.after(0, lambda: self.progress_var.set(0))
|
| 1156 |
-
self.after(0, lambda: self.status_model_var.set("正在
|
| 1157 |
|
| 1158 |
-
# 1
|
| 1159 |
operations = []
|
| 1160 |
for local_file, remote_path in to_upload:
|
| 1161 |
-
# 极度关键:防止 Windows 反斜杠导致远端多建文件夹
|
| 1162 |
safe_remote_path = remote_path.replace("\\", "/")
|
| 1163 |
operations.append(
|
| 1164 |
CommitOperationAdd(
|
|
@@ -1167,55 +1210,141 @@ class ModelsManager(tk.Tk):
|
|
| 1167 |
)
|
| 1168 |
)
|
| 1169 |
|
| 1170 |
-
# 2. 准备 commit message
|
| 1171 |
remote_dir = self.remote_dir_var.get().strip().replace("\\", "/").strip("/")
|
| 1172 |
commit_msg = f"Upload files to {remote_dir}" if remote_dir else "Upload files to root"
|
| 1173 |
-
|
| 1174 |
-
self.log_model(f"开始打包 {len(operations)} 个文件,一次性 Commit...")
|
| 1175 |
-
self.log_model(f"Commit message: {commit_msg}")
|
| 1176 |
|
| 1177 |
-
# 🌟 3. 假进度条控制变量
|
| 1178 |
is_uploading = [True]
|
| 1179 |
def update_progress():
|
| 1180 |
-
if not is_uploading[0]: return
|
| 1181 |
current = self.progress_var.get()
|
| 1182 |
-
# 慢慢爬升到 95%,最后留 5% 给真正完成时冲刺
|
| 1183 |
if current < 95:
|
| 1184 |
self.progress_var.set(current + 2)
|
| 1185 |
self.after(300, update_progress)
|
| 1186 |
-
|
| 1187 |
-
self.after(100, update_progress) # 启动动画
|
| 1188 |
|
| 1189 |
-
|
| 1190 |
api.create_commit(
|
| 1191 |
repo_id=MODEL_REPO_ID,
|
| 1192 |
repo_type="model",
|
| 1193 |
operations=operations,
|
| 1194 |
commit_message=commit_msg
|
| 1195 |
)
|
|
|
|
| 1196 |
|
| 1197 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
is_uploading[0] = False
|
| 1199 |
self.after(0, lambda: self.progress_var.set(100))
|
| 1200 |
self.after(0, lambda: self.status_model_var.set("上传完成 ✅"))
|
| 1201 |
self.after(0, lambda: self.btn_upload.configure(state="normal"))
|
| 1202 |
-
|
| 1203 |
-
self.log_model("打包上传成功!")
|
| 1204 |
-
self.after(0, lambda: messagebox.showinfo("完成", "上传成功!"))
|
| 1205 |
-
self.after(0, self.load_remote) # 自动刷新 JSON
|
| 1206 |
|
|
|
|
|
|
|
| 1207 |
except Exception as e:
|
| 1208 |
-
# 🌟 失败时:停止动画,重置进度条,恢复按钮
|
| 1209 |
is_uploading[0] = False
|
| 1210 |
self.after(0, lambda: self.progress_var.set(0))
|
| 1211 |
self.after(0, lambda: self.btn_upload.configure(state="normal"))
|
| 1212 |
-
self.after(0, lambda: self.status_model_var.set("
|
| 1213 |
-
|
| 1214 |
-
self.log_exc("打包上传失败", is_model=True)
|
| 1215 |
self.after(0, lambda: messagebox.showerror("失败", "详见日志"))
|
| 1216 |
|
| 1217 |
threading.Thread(target=upload_worker, daemon=True).start()
|
| 1218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1219 |
# ==========================================================
|
| 1220 |
# ==================== 区域 3: Dataset 上传 =================
|
| 1221 |
# ==========================================================
|
|
|
|
| 317 |
# 3. 管理全局 KV
|
| 318 |
ttk.Button(top, text="🔧 管理全局 KV", command=self.open_fields_manager).pack(side="left", padx=6)
|
| 319 |
|
| 320 |
+
# 🌟 新增:删除选中条目
|
| 321 |
+
ttk.Button(top, text="❌ 删除选中", command=self.delete_selected_item).pack(side="left", padx=6)
|
| 322 |
+
|
| 323 |
# 4. 加载本地
|
| 324 |
ttk.Button(top, text="📂 加载本地 models.json", command=self.load_local).pack(side="left", padx=6)
|
| 325 |
|
|
|
|
| 474 |
|
| 475 |
ttk.Label(win, text="注意:操作会直接修改内存中的数据,完成后再去点【保存本地】", foreground="red").pack(pady=10)
|
| 476 |
|
| 477 |
+
def delete_selected_item(self):
|
| 478 |
+
"""从内存中删除选中的条目"""
|
| 479 |
+
sel = self.tree.focus()
|
| 480 |
+
if not sel or sel not in self.items:
|
| 481 |
+
messagebox.showwarning("提示", "请先在表格中选中要删除的条目!")
|
| 482 |
+
return
|
| 483 |
+
|
| 484 |
+
if not messagebox.askyesno("确认删除", f"确定要删除这条记录吗?\n\n{sel}\n\n注意:这只会从本地 JSON 中移除,不会删除云端实际的模型文件。"):
|
| 485 |
+
return
|
| 486 |
+
|
| 487 |
+
# 从内存中移除
|
| 488 |
+
self.items.pop(sel, None)
|
| 489 |
+
|
| 490 |
+
# 如果它恰好是新增的,也从 new_paths 里移除
|
| 491 |
+
if sel in self.new_paths:
|
| 492 |
+
self.new_paths.remove(sel)
|
| 493 |
+
|
| 494 |
+
self.log_json(f"🗑️ 已在本地删除条目: {sel}")
|
| 495 |
+
self.refresh_tree()
|
| 496 |
def open_dataset_repo(self):
|
| 497 |
import webbrowser
|
| 498 |
webbrowser.open(f"https://huggingface.co/datasets/{DATASET_REPO_ID}/tree/main")
|
|
|
|
| 648 |
meta.get("showcase", "")
|
| 649 |
]
|
| 650 |
for col in dynamic_cols:
|
| 651 |
+
val = meta.get(col, "")
|
| 652 |
+
# 🌟 新增:如果列名是 update_time,把毫秒时间戳转成可读日期展示
|
| 653 |
+
if col == "update_time" and val:
|
| 654 |
+
try:
|
| 655 |
+
# 转换成本地时间的 YYYY-MM-DD 格式
|
| 656 |
+
import datetime
|
| 657 |
+
dt = datetime.datetime.fromtimestamp(int(val) / 1000.0)
|
| 658 |
+
val = dt.strftime('%Y-%m-%d')
|
| 659 |
+
except Exception:
|
| 660 |
+
pass
|
| 661 |
+
row_values.append(val)
|
| 662 |
|
| 663 |
self.tree.insert("", "end", iid=path, text=path, values=row_values, tags=tags)
|
| 664 |
|
|
|
|
| 779 |
|
| 780 |
# 🌟 核心修改:深拷贝一份用于上传
|
| 781 |
upload_items = copy.deepcopy(sorted_items)
|
| 782 |
+
|
| 783 |
+
# 🌟 精准时间戳策略:只为从未有过时间戳的新文件打上当前时间,老文件保留原本时间
|
| 784 |
+
import time
|
| 785 |
+
current_ts = int(time.time() * 1000)
|
| 786 |
|
| 787 |
for path, meta in upload_items.items():
|
| 788 |
if not isinstance(meta, dict): continue
|
| 789 |
|
| 790 |
+
# 只给全新文件(没有 update_time)打时间戳,绝不覆盖历史时间
|
| 791 |
+
if not meta.get("update_time"):
|
| 792 |
+
meta["update_time"] = current_ts
|
| 793 |
+
|
| 794 |
+
# 剥离前缀
|
| 795 |
+
if meta.get("dataset") and meta["dataset"].startswith(DATASET_BASE):
|
| 796 |
+
stripped = meta["dataset"][len(DATASET_BASE):].lstrip("/")
|
| 797 |
+
if stripped: meta["dataset"] = stripped
|
| 798 |
|
| 799 |
if meta.get("showcase"):
|
| 800 |
+
prefix = f"{MODEL_BASE}/tree/main/{SHOWCASE_DIR}"
|
| 801 |
+
if meta["showcase"].startswith(prefix):
|
| 802 |
+
stripped = meta["showcase"][len(prefix):].lstrip("/")
|
| 803 |
+
if stripped: meta["showcase"] = stripped
|
| 804 |
|
| 805 |
+
if meta.get("guide") and meta["guide"].startswith(GUIDE_BASE):
|
| 806 |
+
stripped = meta["guide"][len(GUIDE_BASE):].lstrip("/")
|
| 807 |
+
if stripped: meta["guide"] = stripped
|
| 808 |
|
| 809 |
tmp = TEMP_UPLOAD_JSON
|
| 810 |
with open(tmp, "w", encoding="utf-8") as f:
|
|
|
|
| 1195 |
try:
|
| 1196 |
from huggingface_hub import CommitOperationAdd
|
| 1197 |
|
|
|
|
| 1198 |
self.after(0, lambda: self.btn_upload.configure(state="disabled"))
|
| 1199 |
self.after(0, lambda: self.progress_var.set(0))
|
| 1200 |
+
self.after(0, lambda: self.status_model_var.set("正在上传模型文件..."))
|
| 1201 |
|
| 1202 |
+
# ========== 步骤 1:上传物理文件 ==========
|
| 1203 |
operations = []
|
| 1204 |
for local_file, remote_path in to_upload:
|
|
|
|
| 1205 |
safe_remote_path = remote_path.replace("\\", "/")
|
| 1206 |
operations.append(
|
| 1207 |
CommitOperationAdd(
|
|
|
|
| 1210 |
)
|
| 1211 |
)
|
| 1212 |
|
|
|
|
| 1213 |
remote_dir = self.remote_dir_var.get().strip().replace("\\", "/").strip("/")
|
| 1214 |
commit_msg = f"Upload files to {remote_dir}" if remote_dir else "Upload files to root"
|
|
|
|
|
|
|
|
|
|
| 1215 |
|
|
|
|
| 1216 |
is_uploading = [True]
|
| 1217 |
def update_progress():
|
| 1218 |
+
if not is_uploading[0]: return
|
| 1219 |
current = self.progress_var.get()
|
|
|
|
| 1220 |
if current < 95:
|
| 1221 |
self.progress_var.set(current + 2)
|
| 1222 |
self.after(300, update_progress)
|
| 1223 |
+
self.after(100, update_progress)
|
|
|
|
| 1224 |
|
| 1225 |
+
self.log_model(f"开始打包 {len(operations)} 个文件,一次性 Commit...")
|
| 1226 |
api.create_commit(
|
| 1227 |
repo_id=MODEL_REPO_ID,
|
| 1228 |
repo_type="model",
|
| 1229 |
operations=operations,
|
| 1230 |
commit_message=commit_msg
|
| 1231 |
)
|
| 1232 |
+
self.log_model("模型文件上传成功!正在同步 models.json...")
|
| 1233 |
|
| 1234 |
+
# ========== 步骤 2:拉取云端最新 JSON ==========
|
| 1235 |
+
self.after(0, lambda: self.status_model_var.set("正在拉取云端最新 JSON..."))
|
| 1236 |
+
|
| 1237 |
+
space_subdomain = SPACE_ID.replace("/", "-").replace("_", "-").lower()
|
| 1238 |
+
url = f"https://{space_subdomain}.static.hf.space/models.json"
|
| 1239 |
+
|
| 1240 |
+
latest_remote_items = {}
|
| 1241 |
+
try:
|
| 1242 |
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 1243 |
+
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as response:
|
| 1244 |
+
remote_data = json.loads(response.read().decode())
|
| 1245 |
+
latest_remote_items = remote_data.get("items", {})
|
| 1246 |
+
self.log_model(f"成功拉取云端 JSON,包含 {len(latest_remote_items)} 条记录。")
|
| 1247 |
+
except Exception as e:
|
| 1248 |
+
self.log_model(f"拉取云端 JSON 失败,放弃本次同步以防止覆盖: {e}")
|
| 1249 |
+
raise e # 如果拉取失败,直接中断,绝不用空数据覆盖!
|
| 1250 |
+
|
| 1251 |
+
# ========== 步骤 3:精准追加新条目 ==========
|
| 1252 |
+
import time
|
| 1253 |
+
current_ts = int(time.time() * 1000)
|
| 1254 |
+
added_count = 0
|
| 1255 |
+
|
| 1256 |
+
for local_file, remote_path in to_upload:
|
| 1257 |
+
safe_remote_path = remote_path.replace("\\", "/")
|
| 1258 |
+
|
| 1259 |
+
# 🚨 绝对严格过滤:只有 .safetensors 才配进入 models.json
|
| 1260 |
+
if not safe_remote_path.endswith(".safetensors"):
|
| 1261 |
+
continue
|
| 1262 |
+
|
| 1263 |
+
# 如果云端已经有这个模型了,更新它的时间;如果没有,则新增
|
| 1264 |
+
if safe_remote_path in latest_remote_items:
|
| 1265 |
+
latest_remote_items[safe_remote_path]["update_time"] = current_ts
|
| 1266 |
+
else:
|
| 1267 |
+
latest_remote_items[safe_remote_path] = {
|
| 1268 |
+
"title": "", "trigger": "", "dataset": "", "showcase": "", "update_time": current_ts
|
| 1269 |
+
}
|
| 1270 |
+
added_count += 1
|
| 1271 |
+
|
| 1272 |
+
self.log_model(f"已为 {added_count} 个新增模型写入时间戳。")
|
| 1273 |
+
|
| 1274 |
+
# 按原有逻辑重新排序
|
| 1275 |
+
sorted_items = {k: latest_remote_items[k] for k in sorted(latest_remote_items.keys(), key=custom_sort_key)}
|
| 1276 |
+
|
| 1277 |
+
# ========== 步骤 4:剥离前缀并推送 ==========
|
| 1278 |
+
if added_count > 0:
|
| 1279 |
+
self.after(0, lambda: self.status_model_var.set("正在推送到云端..."))
|
| 1280 |
+
|
| 1281 |
+
# 补全 URL 后存入本地(保持本地数据完整)
|
| 1282 |
+
self.items = sorted_items
|
| 1283 |
+
expand_urls(self.items)
|
| 1284 |
+
self.save_local_silent()
|
| 1285 |
+
self.after(0, self.refresh_tree)
|
| 1286 |
+
|
| 1287 |
+
# 深拷贝用于剥离上传
|
| 1288 |
+
import copy
|
| 1289 |
+
upload_items = copy.deepcopy(sorted_items)
|
| 1290 |
+
for p, meta in upload_items.items():
|
| 1291 |
+
if not isinstance(meta, dict): continue
|
| 1292 |
+
|
| 1293 |
+
if meta.get("dataset") and meta["dataset"].startswith(DATASET_BASE):
|
| 1294 |
+
stripped = meta["dataset"][len(DATASET_BASE):].lstrip("/")
|
| 1295 |
+
if stripped: meta["dataset"] = stripped
|
| 1296 |
+
if meta.get("showcase"):
|
| 1297 |
+
prefix = f"{MODEL_BASE}/tree/main/{SHOWCASE_DIR}"
|
| 1298 |
+
if meta["showcase"].startswith(prefix):
|
| 1299 |
+
stripped = meta["showcase"][len(prefix):].lstrip("/")
|
| 1300 |
+
if stripped: meta["showcase"] = stripped
|
| 1301 |
+
if meta.get("guide") and meta["guide"].startswith(GUIDE_BASE):
|
| 1302 |
+
stripped = meta["guide"][len(GUIDE_BASE):].lstrip("/")
|
| 1303 |
+
if stripped: meta["guide"] = stripped
|
| 1304 |
+
|
| 1305 |
+
tmp_json = TEMP_UPLOAD_JSON
|
| 1306 |
+
with open(tmp_json, "w", encoding="utf-8") as f:
|
| 1307 |
+
json.dump({"items": upload_items}, f, indent=2, ensure_ascii=False)
|
| 1308 |
+
|
| 1309 |
+
api.upload_file(
|
| 1310 |
+
path_or_fileobj=tmp_json,
|
| 1311 |
+
path_in_repo=OUTPUT_FILE,
|
| 1312 |
+
repo_id=SPACE_ID,
|
| 1313 |
+
repo_type="space",
|
| 1314 |
+
commit_message=f"Update JSON for new model(s)"
|
| 1315 |
+
)
|
| 1316 |
+
try: os.remove(tmp_json)
|
| 1317 |
+
except OSError: pass
|
| 1318 |
+
|
| 1319 |
+
self.log_model("models.json 已同步更新到云端!")
|
| 1320 |
+
|
| 1321 |
+
# ========== 步骤 5:收尾 ==========
|
| 1322 |
is_uploading[0] = False
|
| 1323 |
self.after(0, lambda: self.progress_var.set(100))
|
| 1324 |
self.after(0, lambda: self.status_model_var.set("上传完成 ✅"))
|
| 1325 |
self.after(0, lambda: self.btn_upload.configure(state="normal"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1326 |
|
| 1327 |
+
self.after(0, lambda: messagebox.showinfo("完成", "模型与 JSON 均已安全同步!"))
|
| 1328 |
+
|
| 1329 |
except Exception as e:
|
|
|
|
| 1330 |
is_uploading[0] = False
|
| 1331 |
self.after(0, lambda: self.progress_var.set(0))
|
| 1332 |
self.after(0, lambda: self.btn_upload.configure(state="normal"))
|
| 1333 |
+
self.after(0, lambda: self.status_model_var.set("执行失败 ❌"))
|
| 1334 |
+
self.log_exc("上传或同步失败", is_model=True)
|
|
|
|
| 1335 |
self.after(0, lambda: messagebox.showerror("失败", "详见日志"))
|
| 1336 |
|
| 1337 |
threading.Thread(target=upload_worker, daemon=True).start()
|
| 1338 |
|
| 1339 |
+
|
| 1340 |
+
def save_local_silent(self):
|
| 1341 |
+
"""静默保存本地 JSON,不弹出提示框"""
|
| 1342 |
+
data_items = {k: v for k, v in self.items.items() if k not in self.stale_paths}
|
| 1343 |
+
sorted_items = {k: data_items[k] for k in sorted(data_items.keys(), key=custom_sort_key)}
|
| 1344 |
+
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
| 1345 |
+
json.dump({"items": sorted_items}, f, indent=2, ensure_ascii=False)
|
| 1346 |
+
self.log_json(f"已静默保存本地 {OUTPUT_FILE},{len(sorted_items)} 条")
|
| 1347 |
+
|
| 1348 |
# ==========================================================
|
| 1349 |
# ==================== 区域 3: Dataset 上传 =================
|
| 1350 |
# ==========================================================
|