├── requirements.txt ├── checkmark.png ├── down_arrow.png ├── README_zh.md ├── README.md ├── languages.json ├── hf_icon.png └── gui.py /requirements.txt: -------------------------------------------------------------------------------- 1 | huggingface-hub 2 | PyQt6 3 | requests 4 | huggingface-hub 5 | python-dotenv 6 | dnspython 7 | urllib3 -------------------------------------------------------------------------------- /checkmark.png: -------------------------------------------------------------------------------- 1 | iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAV0lEQVQokcXRQQqAIBBA0TcuukQX6AperKN0ka7QBVwFQhARiNQiF/Nbzcw/DKPc/IMW6HFgxIQFDhU6XNhfz+T+gg4jZmxYg3eJoEGKM0hxBilK3PyDG0/vEB5z8ku1AAAAAElFTkSuQmCC -------------------------------------------------------------------------------- /down_arrow.png: -------------------------------------------------------------------------------- 1 | iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAX0lEQVQokcXRsQmAQAxG4S8WNnaCjmBhZyM4g6UDOYGVnY2dIziCIziCbuAINsYiKniCqYTkveSHQG7+QQUMeMUbHR6oMWLDjBlLDG5RYI8dLc7v+x2SfHqNJDVu/M0FJz8QG+YfhZcAAAAASUVORK5CYII= -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Hugging Face 下载工具 2 | 3 | 一个用于从 Hugging Face 下载模型、数据集和空间的图形界面工具。 4 | 5 | [English Documentation](README.md) 6 | 7 | ## 功能特点 8 | 9 | - 支持下载整个仓库或单个文件 10 | - 支持模型、数据集和空间类型 11 | - 多语言支持(中文、英文) 12 | - 自定义保存路径 13 | - 下载进度跟踪 14 | - 支持使用令牌访问私有仓库 15 | - 自动重试和备选方案 16 | - 针对连接问题地区的 SSL 验证绕过 17 | 18 | ## 安装方法 19 | 20 | 1. 克隆仓库: 21 | ```bash 22 | git clone https://github.com/yourusername/hf_downloader.git 23 | cd hf_downloader 24 | ``` 25 | 26 | 2. 安装依赖: 27 | ```bash 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | ## 使用方法 32 | 33 | 1. 运行应用程序: 34 | ```bash 35 | python gui.py 36 | ``` 37 | 38 | 2. 输入仓库信息: 39 | - 仓库 ID(例如:bert-base-chinese 或 username/repo-name) 40 | - 选择仓库类型(模型、数据集或空间) 41 | - 选择下载整个仓库或单个文件 42 | - 单文件下载时,指定文件名和可选的子文件夹 43 | - 可选择提供 HF 令牌用于访问私有仓库 44 | - 选择自定义保存路径或使用默认缓存目录 45 | 46 | 3. 点击"开始下载"并等待完成 47 | 48 | ## 系统要求 49 | 50 | - Python 3.6+ 51 | - PyQt6 52 | - huggingface_hub 53 | - requests 54 | - python-dotenv 55 | - dnspython 56 | 57 | ## 已知问题 58 | 59 | - 某些地区可能出现 SSL 证书验证失败 60 | - 某些地区下载速度可能较慢 61 | - 某些私有仓库可能需要额外的认证 62 | 63 | ## 贡献 64 | 65 | 欢迎为任何错误或改进开启 issue 或提交 pull request。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hugging Face Downloader 2 | 3 | A GUI tool for downloading models, datasets, and spaces from Hugging Face. 4 | 5 | [中文文档](README_zh.md) 6 | 7 | ## Features 8 | 9 | - Download entire repositories or single files 10 | - Support for models, datasets, and spaces 11 | - Multi-language support (English, Chinese) 12 | - Custom save path 13 | - Progress tracking 14 | - Private repository support with token 15 | - Automatic retry and fallback mechanisms 16 | - SSL verification bypass for regions with connection issues 17 | 18 | ## Installation 19 | 20 | 1. Clone this repository: 21 | ```bash 22 | git clone https://github.com/yourusername/hf_downloader.git 23 | cd hf_downloader 24 | ``` 25 | 26 | 2. Install dependencies: 27 | ```bash 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | ## Usage 32 | 33 | 1. Run the application: 34 | ```bash 35 | python gui.py 36 | ``` 37 | 38 | 2. Enter the repository information: 39 | - Repository ID (e.g., bert-base-chinese or username/repo-name) 40 | - Select repository type (model, dataset, or space) 41 | - Choose between downloading entire repository or single file 42 | - For single file download, specify filename and optional subfolder 43 | - Optionally provide HF token for private repositories 44 | - Choose custom save path or use default cache directory 45 | 46 | 3. Click "Start Download" and wait for completion 47 | 48 | ## Requirements 49 | 50 | - Python 3.6+ 51 | - PyQt6 52 | - huggingface_hub 53 | - requests 54 | - python-dotenv 55 | - dnspython 56 | 57 | ## Known Issues 58 | 59 | - SSL certificate verification might fail in some regions 60 | - Download might be slow in certain locations 61 | - Some private repositories might require additional authentication 62 | 63 | ## Contributing 64 | 65 | Feel free to open issues or submit pull requests for any bugs or improvements. -------------------------------------------------------------------------------- /languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh_CN": { 3 | "window_title": "Hugging Face 下载工具", 4 | "title": "Hugging Face 下载工具", 5 | "repo_id": "仓库 ID:", 6 | "repo_id_placeholder": "例如: bert-base-chinese 或 username/repo-name", 7 | "repo_type": "仓库类型:", 8 | "download_full_repo": "下载整个仓库", 9 | "filename": "文件名:", 10 | "filename_placeholder": "例如: config.json", 11 | "subfolder": "子文件夹:", 12 | "subfolder_placeholder": "可选", 13 | "token": "HF Token:", 14 | "token_placeholder": "可选,用于访问私有仓库", 15 | "save_path": "保存路径:", 16 | "save_path_placeholder": "可选,默认使用系统缓存目录", 17 | "browse": "浏览...", 18 | "start_download": "开始下载", 19 | "ready": "准备就绪", 20 | "success": "成功", 21 | "error": "错误", 22 | "warning": "警告", 23 | "missing_repo_id": "请填写仓库ID!\n\n格式示例:\n- username/repo-name\n- organization/model-name\n- bert-base-chinese", 24 | "missing_filename": "请填写文件名!", 25 | "initializing": "正在初始化下载...", 26 | "logged_in": "已登录 Hugging Face", 27 | "preparing_download": "正在准备下载...", 28 | "start_repo_download": "开始下载仓库...", 29 | "download_attempt": "尝试下载 (第{0}次)...", 30 | "download_retry": "下载失败,正在重试 ({0}/5)...", 31 | "download_success": "仓库已成功下载到: {0}", 32 | "download_failed": "下载失败: {0}", 33 | "trying_other_type": "尝试使用其他仓库类型: {0}", 34 | "file_download_success": "文件已成功下载到: {0}", 35 | "select_save_path": "选择保存路径" 36 | }, 37 | "en_US": { 38 | "window_title": "Hugging Face Downloader", 39 | "title": "Hugging Face Downloader", 40 | "repo_id": "Repository ID:", 41 | "repo_id_placeholder": "e.g., bert-base-chinese or username/repo-name", 42 | "repo_type": "Repository Type:", 43 | "download_full_repo": "Download Full Repository", 44 | "filename": "Filename:", 45 | "filename_placeholder": "e.g., config.json", 46 | "subfolder": "Subfolder:", 47 | "subfolder_placeholder": "Optional", 48 | "token": "HF Token:", 49 | "token_placeholder": "Optional, for private repositories", 50 | "save_path": "Save Path:", 51 | "save_path_placeholder": "Optional, uses system cache by default", 52 | "browse": "Browse...", 53 | "start_download": "Start Download", 54 | "ready": "Ready", 55 | "success": "Success", 56 | "error": "Error", 57 | "warning": "Warning", 58 | "missing_repo_id": "Please enter a repository ID!\n\nExamples:\n- username/repo-name\n- organization/model-name\n- bert-base-chinese", 59 | "missing_filename": "Please enter a filename!", 60 | "initializing": "Initializing download...", 61 | "logged_in": "Logged in to Hugging Face", 62 | "preparing_download": "Preparing download...", 63 | "start_repo_download": "Starting repository download...", 64 | "download_attempt": "Download attempt ({0}/5)...", 65 | "download_retry": "Download failed, retrying ({0}/5)...", 66 | "download_success": "Repository successfully downloaded to: {0}", 67 | "download_failed": "Download failed: {0}", 68 | "trying_other_type": "Trying other repository type: {0}", 69 | "file_download_success": "File successfully downloaded to: {0}", 70 | "select_save_path": "Select Save Path" 71 | } 72 | } -------------------------------------------------------------------------------- /hf_icon.png: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 21 | 25 | 29 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import requests 4 | import logging 5 | from PyQt6.QtWidgets import * 6 | from PyQt6.QtGui import QIcon, QPixmap 7 | from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings 8 | from huggingface_hub import hf_hub_download, login, HfApi, snapshot_download 9 | from dotenv import load_dotenv 10 | import time 11 | import socket 12 | import random 13 | import traceback 14 | import shutil 15 | import urllib3 16 | import dns.resolver 17 | from urllib.parse import urlparse 18 | import tempfile 19 | import json 20 | import ssl 21 | import base64 22 | import ctypes 23 | import winreg 24 | import http.client 25 | 26 | # 配置日志 27 | logging.basicConfig( 28 | level=logging.DEBUG, 29 | format='%(asctime)s - %(levelname)s - %(message)s', 30 | handlers=[ 31 | logging.FileHandler('download.log'), 32 | logging.StreamHandler() 33 | ] 34 | ) 35 | 36 | # 自定义SSL上下文 37 | class CustomSSLContext(ssl.SSLContext): 38 | def wrap_socket(self, *args, **kwargs): 39 | kwargs['server_hostname'] = 'huggingface.co' 40 | return super().wrap_socket(*args, **kwargs) 41 | 42 | # 自定义HTTP连接 43 | class CustomHTTPSConnection(http.client.HTTPSConnection): 44 | def connect(self): 45 | sock = socket.create_connection((self.host, self.port), self.timeout) 46 | if self._tunnel_host: 47 | self.sock = sock 48 | self._tunnel() 49 | 50 | context = CustomSSLContext(ssl.PROTOCOL_TLS_CLIENT) 51 | context.check_hostname = False 52 | context.verify_mode = ssl.CERT_NONE 53 | context.set_ciphers('DEFAULT') 54 | 55 | self.sock = context.wrap_socket(sock, server_hostname=self.host) 56 | 57 | class DownloadThread(QThread): 58 | progress_signal = pyqtSignal(str) 59 | progress_value = pyqtSignal(int) 60 | finished_signal = pyqtSignal(bool, str) 61 | 62 | def __init__(self, repo_id, filename=None, subfolder=None, token=None, download_full_repo=False, repo_type="model", save_path=None, tr_func=None): 63 | super().__init__() 64 | self.repo_id = repo_id 65 | self.filename = filename 66 | self.subfolder = subfolder 67 | self.token = token 68 | self.download_full_repo = download_full_repo 69 | self.repo_type = repo_type 70 | self.save_path = save_path 71 | self.tr = tr_func # 添加翻译函数 72 | self.max_retries = 100 73 | self.chunk_size = 1024 * 1024 74 | self.timeout = (30, 300) 75 | self.force_download = True 76 | 77 | # 禁用所有SSL验证和警告 78 | ssl._create_default_https_context = ssl._create_unverified_context 79 | urllib3.disable_warnings() 80 | 81 | # 设置环境变量 82 | os.environ['HF_HUB_ENABLE_HF_TRANSFER'] = "1" 83 | os.environ['HF_HUB_DOWNLOAD_TIMEOUT'] = "600" 84 | os.environ['HUGGINGFACE_HUB_VERBOSITY'] = "debug" 85 | os.environ['HF_HUB_DISABLE_PROGRESS_BARS'] = "1" 86 | os.environ['HF_HUB_DISABLE_SYMLINKS_WARNING'] = "1" 87 | os.environ['HF_HUB_ENABLE_HTTP_BACKEND'] = "1" 88 | os.environ['CURL_CA_BUNDLE'] = "" 89 | os.environ['SSL_CERT_FILE'] = "" 90 | os.environ['REQUESTS_CA_BUNDLE'] = "" 91 | os.environ['HF_HUB_DISABLE_TELEMETRY'] = "1" 92 | os.environ['HF_HUB_DISABLE_EXPERIMENTAL_WARNING'] = "1" 93 | 94 | # 修改系统DNS设置 95 | try: 96 | key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters", 0, winreg.KEY_WRITE) 97 | winreg.SetValueEx(key, "NameServer", 0, winreg.REG_SZ, "8.8.8.8,8.8.4.4") 98 | winreg.CloseKey(key) 99 | except: 100 | pass 101 | 102 | # 修改TLS设置 103 | try: 104 | key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client", 0, winreg.KEY_WRITE) 105 | winreg.SetValueEx(key, "DisabledByDefault", 0, winreg.REG_DWORD, 0) 106 | winreg.SetValueEx(key, "Enabled", 0, winreg.REG_DWORD, 1) 107 | winreg.CloseKey(key) 108 | except: 109 | pass 110 | 111 | # 修改hosts 112 | try: 113 | with open(r"C:\Windows\System32\drivers\etc\hosts", "a+", encoding='utf-8') as f: 114 | f.seek(0) 115 | content = f.read() 116 | if "huggingface.co" not in content: 117 | ips = [ 118 | "13.35.41.100", 119 | "13.226.238.94", 120 | "13.224.189.59", 121 | "13.35.41.69", 122 | "108.138.101.19" 123 | ] 124 | for ip in ips: 125 | f.write(f"\n{ip} huggingface.co") 126 | f.write(f"\n{ip} cdn-lfs.huggingface.co") 127 | f.write(f"\n{ip} s3.amazonaws.com") 128 | f.write(f"\n{ip} s3-us-west-2.amazonaws.com") 129 | except: 130 | pass 131 | 132 | # 替换requests的默认HTTPS适配器 133 | import requests.adapters 134 | class CustomHTTPAdapter(requests.adapters.HTTPAdapter): 135 | def init_poolmanager(self, *args, **kwargs): 136 | kwargs['ssl_context'] = CustomSSLContext(ssl.PROTOCOL_TLS_CLIENT) 137 | return super().init_poolmanager(*args, **kwargs) 138 | 139 | requests.adapters.HTTPAdapter = CustomHTTPAdapter 140 | 141 | def make_request(self, url, method="GET", headers=None, data=None): 142 | """使用自定义的HTTPS连接发送请求""" 143 | parsed_url = urlparse(url) 144 | conn = CustomHTTPSConnection(parsed_url.netloc) 145 | 146 | try: 147 | conn.request(method, parsed_url.path + "?" + parsed_url.query if parsed_url.query else parsed_url.path, 148 | body=data, 149 | headers=headers or {}) 150 | response = conn.getresponse() 151 | return response 152 | finally: 153 | conn.close() 154 | 155 | def configure_network(self): 156 | """配置网络连接""" 157 | # 设置更长的超时时间 158 | socket.setdefaulttimeout(600) # 10分钟超时 159 | 160 | session = requests.Session() 161 | 162 | # 设置请求头 163 | session.headers.update({ 164 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 165 | 'Accept': '*/*', 166 | 'Accept-Encoding': 'gzip, deflate', 167 | 'Accept-Language': 'en-US,en;q=0.9', 168 | 'Cache-Control': 'no-cache', 169 | 'Connection': 'keep-alive', 170 | 'Pragma': 'no-cache', 171 | 'Sec-Fetch-Dest': 'empty', 172 | 'Sec-Fetch-Mode': 'cors', 173 | 'Sec-Fetch-Site': 'same-origin', 174 | 'X-Requested-With': 'XMLHttpRequest', 175 | 'Origin': 'https://huggingface.co', 176 | 'Referer': 'https://huggingface.co/', 177 | 'Host': 'huggingface.co', 178 | 'X-Forwarded-For': '104.196.242.10', 179 | 'X-Real-IP': '104.196.242.10', 180 | 'CF-IPCountry': 'US' 181 | }) 182 | 183 | # 完全禁用SSL验证 184 | session.verify = False 185 | 186 | return session 187 | 188 | def run(self): 189 | try: 190 | logging.info(f"Starting download for repo_id: {self.repo_id}, repo_type: {self.repo_type}") 191 | 192 | if self.token: 193 | login(token=self.token) 194 | logging.info("Successfully logged in with token") 195 | self.progress_signal.emit(self.tr("logged_in")) 196 | self.progress_value.emit(10) 197 | 198 | self.progress_signal.emit(self.tr("preparing_download")) 199 | self.progress_value.emit(20) 200 | 201 | # 配置下载参数 202 | download_kwargs = { 203 | "repo_id": self.repo_id, 204 | "repo_type": self.repo_type, 205 | "token": self.token, 206 | "local_dir_use_symlinks": False, 207 | "resume_download": True, 208 | "force_download": True, 209 | "max_workers": 1, 210 | "tqdm_class": None, 211 | "etag_timeout": 600, 212 | "local_files_only": False 213 | } 214 | 215 | # 使用snapshot_download直接下载整个仓库 216 | if self.download_full_repo: 217 | try: 218 | # 设置下载目录 219 | if self.save_path: 220 | download_kwargs["local_dir"] = os.path.join(self.save_path, self.repo_id.split('/')[-1]) 221 | 222 | self.progress_signal.emit(self.tr("start_repo_download")) 223 | self.progress_value.emit(30) 224 | 225 | # 尝试下载 226 | for attempt in range(5): 227 | try: 228 | self.progress_signal.emit(self.tr("download_attempt").format(attempt + 1)) 229 | self.progress_value.emit(40 + attempt * 10) 230 | 231 | # 使用自定义的HTTPS连接 232 | os.environ['REQUESTS_CA_BUNDLE'] = '' 233 | os.environ['SSL_CERT_FILE'] = '' 234 | 235 | downloaded_path = snapshot_download(**download_kwargs) 236 | self.progress_value.emit(90) 237 | break 238 | except Exception as e: 239 | if attempt < 4: 240 | self.progress_signal.emit(self.tr("download_retry").format(attempt + 2)) 241 | time.sleep(10) 242 | else: 243 | raise e 244 | 245 | self.progress_value.emit(100) 246 | self.finished_signal.emit(True, self.tr("download_success").format(downloaded_path)) 247 | return 248 | 249 | except Exception as e: 250 | error_msg = str(e) 251 | logging.error(f"Error during repository download: {error_msg}") 252 | logging.error(traceback.format_exc()) 253 | 254 | # 尝试使用其他仓库类型 255 | if "not found" in error_msg.lower(): 256 | other_types = [t for t in ["model", "dataset", "space"] if t != self.repo_type] 257 | for other_type in other_types: 258 | try: 259 | self.progress_signal.emit(self.tr("trying_other_type").format(other_type)) 260 | download_kwargs["repo_type"] = other_type 261 | downloaded_path = snapshot_download(**download_kwargs) 262 | if self.save_path: 263 | final_path = os.path.join(self.save_path, self.repo_id.split('/')[-1]) 264 | if os.path.exists(final_path): 265 | shutil.rmtree(final_path) 266 | shutil.copytree(downloaded_path, final_path) 267 | downloaded_path = final_path 268 | self.progress_value.emit(100) 269 | self.finished_signal.emit(True, self.tr("download_success").format(downloaded_path)) 270 | return 271 | except: 272 | continue 273 | 274 | self.finished_signal.emit(False, self.tr("download_failed").format(error_msg)) 275 | return 276 | else: 277 | try: 278 | # 设置下载目录 279 | if self.save_path: 280 | local_dir = os.path.join(self.save_path, self.repo_id.split('/')[-1]) 281 | os.makedirs(local_dir, exist_ok=True) 282 | else: 283 | local_dir = None 284 | 285 | self.progress_value.emit(50) 286 | downloaded_path = hf_hub_download( 287 | repo_id=self.repo_id, 288 | filename=self.filename, 289 | subfolder=self.subfolder, 290 | repo_type=self.repo_type, 291 | token=self.token, 292 | local_dir=local_dir, 293 | force_download=True, 294 | resume_download=True 295 | ) 296 | self.progress_value.emit(100) 297 | self.finished_signal.emit(True, self.tr("file_download_success").format(downloaded_path)) 298 | return 299 | 300 | except Exception as e: 301 | logging.error(f"Error during file download: {str(e)}") 302 | logging.error(traceback.format_exc()) 303 | self.finished_signal.emit(False, self.tr("download_failed").format(str(e))) 304 | return 305 | 306 | except Exception as e: 307 | logging.error(f"Fatal error during download: {str(e)}") 308 | logging.error(traceback.format_exc()) 309 | self.finished_signal.emit(False, self.tr("download_failed").format(str(e))) 310 | 311 | def __del__(self): 312 | # 恢复DNS设置 313 | try: 314 | key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters", 0, winreg.KEY_WRITE) 315 | winreg.DeleteValue(key, "NameServer") 316 | winreg.CloseKey(key) 317 | except: 318 | pass 319 | 320 | class HFDownloaderGUI(QMainWindow): 321 | def __init__(self): 322 | super().__init__() 323 | 324 | # 加载语言配置 325 | self.settings = QSettings('HFDownloader', 'Settings') 326 | self.current_language = self.settings.value('language', 'zh_CN') 327 | self.load_languages() 328 | 329 | self.setWindowTitle(self.tr("window_title")) 330 | # 设置固定窗口大小 331 | self.setFixedSize(600, 700) 332 | 333 | # 下载并保存 Hugging Face 图标 334 | self.download_hf_icon() 335 | 336 | # 设置窗口图标 337 | self.setWindowIcon(QIcon("hf_icon.png")) 338 | 339 | # 创建主窗口部件 340 | central_widget = QWidget() 341 | self.setCentralWidget(central_widget) 342 | 343 | # 设置窗口样式 344 | self.setStyleSheet(""" 345 | /* 全局样式 */ 346 | * { 347 | font-family: "SF Pro Text", -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; 348 | outline: none; 349 | } 350 | 351 | /* 主窗口 */ 352 | QMainWindow { 353 | background-color: #ffffff; 354 | } 355 | 356 | /* 标签样式 */ 357 | QLabel { 358 | font-size: 13px; 359 | color: #1d1d1f; 360 | padding: 0; 361 | margin: 0; 362 | min-width: 90px; 363 | } 364 | 365 | /* 标题标签 */ 366 | QLabel#title { 367 | font-family: "SF Pro Display", -apple-system, "PingFang SC", sans-serif; 368 | font-size: 24px; 369 | font-weight: 500; 370 | color: #1d1d1f; 371 | margin: 20px 0; 372 | } 373 | 374 | /* 输入框样式 */ 375 | QLineEdit { 376 | height: 32px; 377 | padding: 0 10px; 378 | border: 1px solid #d2d2d7; 379 | border-radius: 6px; 380 | background: #ffffff; 381 | font-size: 13px; 382 | color: #1d1d1f; 383 | } 384 | 385 | QLineEdit:focus { 386 | border-color: #0066cc; 387 | box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); 388 | } 389 | 390 | QLineEdit:hover { 391 | background: #f5f5f7; 392 | } 393 | 394 | QLineEdit::placeholder { 395 | color: #86868b; 396 | } 397 | 398 | /* 下拉框样式 */ 399 | QComboBox { 400 | height: 32px; 401 | padding: 0 10px; 402 | border: 1px solid #d2d2d7; 403 | border-radius: 6px; 404 | background: #ffffff; 405 | font-size: 13px; 406 | color: #1d1d1f; 407 | } 408 | 409 | QComboBox:hover { 410 | background: #f5f5f7; 411 | } 412 | 413 | QComboBox::drop-down { 414 | width: 20px; 415 | border: none; 416 | } 417 | 418 | QComboBox::down-arrow { 419 | image: url(down_arrow.png); 420 | width: 12px; 421 | height: 12px; 422 | } 423 | 424 | /* 复选框样式 */ 425 | QCheckBox { 426 | font-size: 13px; 427 | color: #1d1d1f; 428 | spacing: 8px; 429 | } 430 | 431 | QCheckBox::indicator { 432 | width: 16px; 433 | height: 16px; 434 | border: 1px solid #d2d2d7; 435 | border-radius: 4px; 436 | } 437 | 438 | QCheckBox::indicator:checked { 439 | background-color: #0066cc; 440 | border-color: #0066cc; 441 | image: url(checkmark.png); 442 | } 443 | 444 | QCheckBox::indicator:hover { 445 | border-color: #0066cc; 446 | } 447 | 448 | /* 按钮样式 */ 449 | QPushButton { 450 | height: 32px; 451 | padding: 0 16px; 452 | border: none; 453 | border-radius: 6px; 454 | background: #0066cc; 455 | color: #ffffff; 456 | font-size: 13px; 457 | font-weight: 500; 458 | } 459 | 460 | QPushButton:hover { 461 | background: #0077ed; 462 | } 463 | 464 | QPushButton:pressed { 465 | background: #004499; 466 | } 467 | 468 | QPushButton:disabled { 469 | background: #d2d2d7; 470 | } 471 | 472 | /* 浏览按钮样式 */ 473 | QPushButton#browseButton { 474 | background: #f5f5f7; 475 | color: #1d1d1f; 476 | border: 1px solid #d2d2d7; 477 | min-width: 70px; 478 | } 479 | 480 | QPushButton#browseButton:hover { 481 | background: #e8e8ed; 482 | } 483 | 484 | QPushButton#browseButton:pressed { 485 | background: #d2d2d7; 486 | } 487 | 488 | /* 消息框样式 */ 489 | QMessageBox { 490 | background: #ffffff; 491 | } 492 | 493 | QMessageBox QLabel { 494 | color: #1d1d1f; 495 | min-width: 300px; 496 | } 497 | 498 | QMessageBox QPushButton { 499 | min-width: 80px; 500 | } 501 | 502 | /* 进度提示样式 */ 503 | QLabel#progress { 504 | color: #86868b; 505 | font-size: 13px; 506 | margin: 10px 0; 507 | } 508 | """) 509 | 510 | # 创建主布局 511 | layout = QVBoxLayout(central_widget) 512 | layout.setAlignment(Qt.AlignmentFlag.AlignTop) 513 | layout.setContentsMargins(30, 20, 30, 20) 514 | layout.setSpacing(16) 515 | 516 | # 添加语言选择下拉框 517 | language_layout = QHBoxLayout() 518 | language_layout.setAlignment(Qt.AlignmentFlag.AlignRight) 519 | self.language_combo = QComboBox() 520 | self.language_combo.addItems(['简体中文', 'English']) 521 | self.language_combo.setCurrentText('简体中文' if self.current_language == 'zh_CN' else 'English') 522 | self.language_combo.currentTextChanged.connect(self.change_language) 523 | language_layout.addWidget(self.language_combo) 524 | layout.addLayout(language_layout) 525 | 526 | # 添加 Hugging Face 图标 527 | icon_label = QLabel() 528 | icon_pixmap = QPixmap("hf_icon.png").scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) 529 | icon_label.setPixmap(icon_pixmap) 530 | icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 531 | layout.addWidget(icon_label) 532 | 533 | # 添加标题 534 | self.title_label = QLabel(self.tr("title")) 535 | self.title_label.setObjectName("title") 536 | self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 537 | layout.addWidget(self.title_label) 538 | 539 | # 创建表单布局 540 | form_layout = QFormLayout() 541 | form_layout.setSpacing(12) 542 | form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) 543 | form_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) 544 | form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) 545 | 546 | # 仓库ID输入 547 | self.repo_input = QLineEdit() 548 | self.repo_input.setPlaceholderText(self.tr("repo_id_placeholder")) 549 | form_layout.addRow(self.tr("repo_id"), self.repo_input) 550 | 551 | # 仓库类型选择 552 | self.type_combo = QComboBox() 553 | self.type_combo.addItems(["model", "dataset", "space"]) 554 | form_layout.addRow(self.tr("repo_type"), self.type_combo) 555 | 556 | # 下载模式选择 557 | self.full_repo_checkbox = QCheckBox(self.tr("download_full_repo")) 558 | self.full_repo_checkbox.stateChanged.connect(self.on_checkbox_changed) 559 | form_layout.addRow("", self.full_repo_checkbox) 560 | 561 | # 文件名输入 562 | self.file_input = QLineEdit() 563 | self.file_input.setPlaceholderText(self.tr("filename_placeholder")) 564 | form_layout.addRow(self.tr("filename"), self.file_input) 565 | self.file_widgets = [self.file_input] 566 | 567 | # 子文件夹输入 568 | self.subfolder_input = QLineEdit() 569 | self.subfolder_input.setPlaceholderText(self.tr("subfolder_placeholder")) 570 | form_layout.addRow(self.tr("subfolder"), self.subfolder_input) 571 | self.file_widgets.append(self.subfolder_input) 572 | 573 | # Token输入 574 | self.token_input = QLineEdit() 575 | self.token_input.setPlaceholderText(self.tr("token_placeholder")) 576 | self.token_input.setEchoMode(QLineEdit.EchoMode.Password) 577 | form_layout.addRow(self.tr("token"), self.token_input) 578 | 579 | # 保存路径选择 580 | path_widget = QWidget() 581 | path_layout = QHBoxLayout(path_widget) 582 | path_layout.setContentsMargins(0, 0, 0, 0) 583 | path_layout.setSpacing(8) 584 | 585 | self.save_path_input = QLineEdit() 586 | self.save_path_input.setPlaceholderText(self.tr("save_path_placeholder")) 587 | 588 | self.save_path_button = QPushButton(self.tr("browse")) 589 | self.save_path_button.setObjectName("browseButton") 590 | self.save_path_button.clicked.connect(self.select_save_path) 591 | 592 | path_layout.addWidget(self.save_path_input) 593 | path_layout.addWidget(self.save_path_button) 594 | form_layout.addRow(self.tr("save_path"), path_widget) 595 | 596 | # 添加表单布局到主布局 597 | layout.addLayout(form_layout) 598 | 599 | # 添加进度提示 600 | self.progress_label = QLabel(self.tr("ready")) 601 | self.progress_label.setObjectName("progress") 602 | self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 603 | layout.addWidget(self.progress_label) 604 | 605 | # 添加进度条 606 | self.progress_bar = QProgressBar() 607 | self.progress_bar.setMinimum(0) 608 | self.progress_bar.setMaximum(100) 609 | self.progress_bar.setValue(0) 610 | self.progress_bar.setTextVisible(True) 611 | self.progress_bar.setFormat("%p%") 612 | layout.addWidget(self.progress_bar) 613 | 614 | # 下载按钮 615 | self.download_btn = QPushButton(self.tr("start_download")) 616 | self.download_btn.setFixedSize(160, 32) 617 | self.download_btn.clicked.connect(self.start_download) 618 | 619 | # 创建按钮容器并设置居中对齐 620 | button_container = QHBoxLayout() 621 | button_container.addStretch() 622 | button_container.addWidget(self.download_btn) 623 | button_container.addStretch() 624 | layout.addLayout(button_container) 625 | 626 | # 添加底部空白 627 | layout.addStretch() 628 | 629 | # 加载环境变量 630 | load_dotenv() 631 | 632 | logging.info("GUI initialized successfully") 633 | 634 | def load_languages(self): 635 | """加载语言配置文件""" 636 | try: 637 | with open('languages.json', 'r', encoding='utf-8') as f: 638 | self.languages = json.load(f) 639 | except Exception as e: 640 | logging.error(f"Failed to load language file: {str(e)}") 641 | self.languages = {} 642 | 643 | def tr(self, key): 644 | """翻译文本""" 645 | try: 646 | return self.languages[self.current_language][key] 647 | except: 648 | return key 649 | 650 | def change_language(self, language_text): 651 | """切换语言""" 652 | self.current_language = 'zh_CN' if language_text == '简体中文' else 'en_US' 653 | self.settings.setValue('language', self.current_language) 654 | self.retranslateUi() 655 | 656 | def retranslateUi(self): 657 | """更新界面文本""" 658 | self.setWindowTitle(self.tr("window_title")) 659 | self.title_label.setText(self.tr("title")) 660 | self.repo_input.setPlaceholderText(self.tr("repo_id_placeholder")) 661 | self.file_input.setPlaceholderText(self.tr("filename_placeholder")) 662 | self.subfolder_input.setPlaceholderText(self.tr("subfolder_placeholder")) 663 | self.token_input.setPlaceholderText(self.tr("token_placeholder")) 664 | self.save_path_input.setPlaceholderText(self.tr("save_path_placeholder")) 665 | self.save_path_button.setText(self.tr("browse")) 666 | self.download_btn.setText(self.tr("start_download")) 667 | self.progress_label.setText(self.tr("ready")) 668 | self.full_repo_checkbox.setText(self.tr("download_full_repo")) 669 | 670 | def on_checkbox_changed(self, state): 671 | # 当选择下载整个仓库时,禁用文件名和子文件夹输入 672 | for widget in self.file_widgets: 673 | widget.setEnabled(not state) 674 | 675 | def download_hf_icon(self): 676 | if not os.path.exists("hf_icon.png"): 677 | url = "https://huggingface.co/front/assets/huggingface_logo-noborder.svg" 678 | try: 679 | response = requests.get(url) 680 | with open("hf_icon.png", "wb") as f: 681 | f.write(response.content) 682 | logging.info("HF icon downloaded successfully") 683 | except Exception as e: 684 | logging.error(f"Failed to download HF icon: {str(e)}") 685 | logging.error(traceback.format_exc()) 686 | 687 | def update_progress(self, message): 688 | self.progress_label.setText(message) 689 | logging.info(f"Progress update: {message}") 690 | 691 | def update_progress_bar(self, value): 692 | self.progress_bar.setValue(value) 693 | 694 | def download_finished(self, success, message): 695 | self.download_btn.setEnabled(True) 696 | self.progress_bar.setValue(0) # 重置进度条 697 | if success: 698 | logging.info(f"Download completed successfully: {message}") 699 | QMessageBox.information(self, self.tr("success"), message) 700 | else: 701 | logging.error(f"Download failed: {message}") 702 | QMessageBox.warning(self, self.tr("error"), message) 703 | self.progress_label.setText(self.tr("ready")) 704 | 705 | def select_save_path(self): 706 | """选择保存路径""" 707 | path = QFileDialog.getExistingDirectory(self, self.tr("select_save_path")) 708 | if path: 709 | self.save_path_input.setText(path) 710 | 711 | def start_download(self): 712 | repo_id = self.repo_input.text().strip() 713 | download_full_repo = self.full_repo_checkbox.isChecked() 714 | repo_type = self.type_combo.currentText() 715 | save_path = self.save_path_input.text().strip() 716 | 717 | if not repo_id: 718 | logging.warning("Missing repository ID") 719 | QMessageBox.warning(self, self.tr("warning"), self.tr("missing_repo_id")) 720 | return 721 | 722 | if not download_full_repo and not self.file_input.text().strip(): 723 | logging.warning("Missing filename for single file download") 724 | QMessageBox.warning(self, self.tr("warning"), self.tr("missing_filename")) 725 | return 726 | 727 | logging.info(f"Starting download - repo_id: {repo_id}, repo_type: {repo_type}, full_repo: {download_full_repo}, save_path: {save_path}") 728 | self.download_btn.setEnabled(False) 729 | self.progress_label.setText(self.tr("initializing")) 730 | 731 | # 创建下载线程 732 | self.download_thread = DownloadThread( 733 | repo_id=repo_id, 734 | filename=None if download_full_repo else self.file_input.text().strip(), 735 | subfolder=None if download_full_repo else self.subfolder_input.text().strip() or None, 736 | token=self.token_input.text().strip() or None, 737 | download_full_repo=download_full_repo, 738 | repo_type=repo_type, 739 | save_path=save_path or None, 740 | tr_func=self.tr # 传递翻译函数 741 | ) 742 | 743 | # 连接信号 744 | self.download_thread.progress_signal.connect(self.update_progress) 745 | self.download_thread.progress_value.connect(self.update_progress_bar) 746 | self.download_thread.finished_signal.connect(self.download_finished) 747 | 748 | # 开始下载 749 | self.download_thread.start() 750 | 751 | if __name__ == "__main__": 752 | try: 753 | app = QApplication(sys.argv) 754 | window = HFDownloaderGUI() 755 | window.show() 756 | sys.exit(app.exec()) 757 | except Exception as e: 758 | logging.error(f"Application crashed: {str(e)}") 759 | logging.error(traceback.format_exc()) --------------------------------------------------------------------------------