import argparse import os import re import json import random import threading import queue from typing import Dict, Tuple, List, Optional, Set import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # ============================================================ # 1. 基础配置 # ============================================================ BASE_URL = "https://modelhub.org.cn" LOGIN_ENDPOINT = "/adminApi/user/login" ADD_ADAPT_TASK_ENDPOINT = "/api/adapt/task/add" MODELHUB_DB_CHECK_ENDPOINT = "/api/computility/models/list/page/vo" TASK_TYPE = "text-generation" FRAMEWORK = "llamacpp" # 按当前策略部署约定,ModelHub 登录凭据固定写入策略代码。 USER_ACCOUNT = "1326356020@qq.com" USER_PASSWORD = "4pdpassword" # 不使用手动 token,统一用账号密码登录获取 token MODELHUB_TOKEN = "" # 平台环境变量契约:提交任务时必须携带当前策略 ID。 STRATEGY_ID = os.environ["STRATEGY_ID"] # ============================================================ # 2. Hugging Face / hf-mirror 筛选配置 # ============================================================ ORG_NAME = "mradermacher" HF_API_URL = "https://hf-mirror.com/api/models" PER_PAGE = int(os.getenv("HF_PER_PAGE", "100")) START_PAGE = int(os.getenv("HF_START_PAGE", "1")) END_PAGE = int(os.getenv("HF_END_PAGE", "0")) # 0 表示持续翻页直到末页 REQUEST_DELAY = 1 # 每次请求的基础延迟(秒),可根据情况调整 RANDOM_DELAY_RANGE = (0.5, 2) # 随机延迟范围(秒),避免固定间隔被识别 RETRY_TIMES = 3 # 429错误重试次数 RETRY_DELAY = 5 # 每次重试的等待时间(秒) MAX_FILE_SIZE_GB = 12 # ============================================================ # 3. 提交与轮询配置 # ============================================================ # 如果所有模型都想强制用某种 GPU,就填: # FORCE_TARGET_GPU = "Ascend_910-b4" # 如果保留 None,则 i1/i2/... 模型走 Ascend_910-b4,其他走 Mthreads_s4000 FORCE_TARGET_GPU = None SUBMIT_INTERVAL_SECONDS = 2 TASK_LIMIT_CODE = 60007 POLL_INTERVAL_SECONDS = 120 MAX_LIMIT_RETRY_TIMES = None # None 表示一直等到有空位 PIPELINE_MODE = os.getenv("PIPELINE_MODE", "continuous").strip().lower() SCAN_INTERVAL_SECONDS = int(os.getenv("PIPELINE_SCAN_INTERVAL_SECONDS", "1800")) MAX_SCAN_PAGES = int(os.getenv("HF_MAX_PAGES", "0")) # 0 表示不限制 RETRY_FAILED = os.getenv("PIPELINE_RETRY_FAILED", "true").strip().lower() in { "1", "true", "yes", "on" } SHUTDOWN_TIMEOUT_SECONDS = 25 DEBUG_PRINT_PAYLOAD = False # 是否开启断点续跑:已经成功提交过的模型,下次运行自动跳过 RESUME = True # ============================================================ # 断点续跑 / 重跑配置 # ============================================================ # run_mode 说明: # resume:优先复用已有步骤产物;缺失时才运行该步骤。适合 Step 4 报错后接着跑。 # fresh :从 start_step 开始重跑,并覆盖对应步骤产物;start_step 之前的步骤读取已有产物。 # 命令行参数优先级高于这里的默认值,例如: # python modelhub_pipeline_resumable.py --start-step 4 --run-mode resume DEFAULT_RUN_MODE = os.getenv("PIPELINE_RUN_MODE", "resume") DEFAULT_START_STEP = int(os.getenv("PIPELINE_START_STEP", "1")) DEFAULT_FORCE_RERUN_STEPS = os.getenv("PIPELINE_FORCE_RERUN_STEPS", "") OUTPUT_DIR = "pipeline_outputs" ALL_MODELS_FILE = os.path.join(OUTPUT_DIR, "01_all_models.txt") SIZE_FILTERED_FILE = os.path.join(OUTPUT_DIR, "02_size_filtered_models.txt") NOT_IN_DB_FILE = os.path.join(OUTPUT_DIR, "03_not_in_db_models.txt") SUBMITTED_FILE = os.path.join(OUTPUT_DIR, "04_submitted_models.txt") FAILED_FILE = os.path.join(OUTPUT_DIR, "05_failed_models.txt") SUMMARY_FILE = os.path.join(OUTPUT_DIR, "summary.json") BASE_HEADERS = { "Content-Type": "application/json", "Accept": "application/json, text/plain, */*", "Origin": BASE_URL, "Referer": BASE_URL + "/", "User-Agent": "Mozilla/5.0", } _shutdown_event = threading.Event() class ShutdownRequested(Exception): """平台请求策略进程优雅停止。""" def request_shutdown() -> None: _shutdown_event.set() def reset_shutdown() -> None: _shutdown_event.clear() def check_shutdown() -> None: if _shutdown_event.is_set(): raise ShutdownRequested("收到停机信号") def interruptible_sleep(seconds: float) -> None: if _shutdown_event.wait(max(0, seconds)): raise ShutdownRequested("收到停机信号") # ============================================================ # 4. 通用工具 # ============================================================ def ensure_output_dir() -> None: os.makedirs(OUTPUT_DIR, exist_ok=True) def write_model_list(path: str, models: List[str]) -> None: with open(path, "w", encoding="utf-8") as f: for model in models: f.write(f'"{model}",\n') def append_model(path: str, model: str) -> None: with open(path, "a", encoding="utf-8") as f: f.write(f'"{model}",\n') def load_model_set(path: str) -> Set[str]: if not os.path.exists(path): return set() with open(path, "r", encoding="utf-8") as f: content = f.read() quoted_items = re.findall(r'["\']([^"\']+)["\']', content) if quoted_items: return set(x.strip() for x in quoted_items if x.strip()) result = set() for line in content.splitlines(): item = line.strip().strip(",").strip('"').strip("'").strip() if item: result.add(item) return result def load_model_list(path: str) -> List[str]: """读取模型列表,保持顺序并去重。""" if not os.path.exists(path): raise FileNotFoundError( f"找不到断点文件: {path}。如果要从中间步骤继续,请先确认前置步骤已成功生成该文件。" ) with open(path, "r", encoding="utf-8") as f: content = f.read() quoted_items = re.findall(r'["\']([^"\']+)["\']', content) if quoted_items: raw_items = [x.strip() for x in quoted_items if x.strip()] else: raw_items = [] for line in content.splitlines(): item = line.strip().strip(",").strip('"').strip("'").strip() if item: raw_items.append(item) seen = set() result = [] for item in raw_items: if item not in seen: result.append(item) seen.add(item) return result def parse_bool_env(value: str, default: bool = False) -> bool: if value is None or value == "": return default return value.strip().lower() in {"1", "true", "yes", "y", "on"} def parse_step_set(value: str) -> Set[int]: result: Set[int] = set() if not value: return result for item in value.split(","): item = item.strip() if not item: continue try: step = int(item) except ValueError: raise ValueError(f"PIPELINE_FORCE_RERUN_STEPS 里包含非法步骤: {item}") if step not in {1, 2, 3}: raise ValueError("可强制重跑的步骤只能是 1、2、3;Step 4 每次都会按断点提交剩余模型。") result.add(step) return result def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="HF / hf-mirror -> ModelHub 断点续跑脚本" ) parser.add_argument( "--run-mode", choices=["resume", "fresh"], default=DEFAULT_RUN_MODE, help="resume=复用已有步骤结果;fresh=从 start-step 开始重跑。默认读取 PIPELINE_RUN_MODE 或 resume。", ) parser.add_argument( "--start-step", type=int, choices=[1, 2, 3, 4], default=DEFAULT_START_STEP, help="从第几步开始。Step 4 报错后继续跑用 --start-step 4。默认读取 PIPELINE_START_STEP 或 1。", ) parser.add_argument( "--force-rerun-steps", default=DEFAULT_FORCE_RERUN_STEPS, help="在 resume 模式下强制重跑指定前置步骤,例如 '2,3'。Step 4 不需要写入这里。", ) parser.add_argument( "--reset-submitted", action="store_true", help="清空 04_submitted_models.txt。谨慎使用:会导致已成功提交过的模型不再被跳过。", ) parser.add_argument( "--reset-failed", action="store_true", help="清空 05_failed_models.txt。适合修复参数后重新统计失败模型。", ) parser.add_argument( "--skip-failed", action="store_true", help="跳过 05_failed_models.txt 中记录过失败的模型。默认不跳过,便于修复问题后自动重试失败模型。", ) args = parser.parse_args() args.force_rerun_steps = parse_step_set(args.force_rerun_steps) return args def should_run_step(step: int, output_file: str, args: argparse.Namespace) -> bool: """ 判断 Step 1-3 是否需要执行。 resume:如果产物存在就读取,不存在才执行;force-rerun-steps 可强制执行。 fresh :start_step 之前读取已有产物,从 start_step 开始重新执行并覆盖产物。 """ if step in args.force_rerun_steps: return True if args.run_mode == "fresh": return step >= args.start_step # resume mode if step < args.start_step: return False return not os.path.exists(output_file) def print_step_loaded(step: int, path: str, models: List[str]) -> None: print(f"Step {step}/4: 跳过执行,读取已有断点文件: {path}") print(f"读取到 {len(models)} 个模型") def remove_file_if_exists(path: str) -> None: if os.path.exists(path): os.remove(path) print(f"已清空: {path}") def create_hf_session() -> requests.Session: session = requests.Session() retry_strategy = Retry( total=RETRY_TIMES, backoff_factor=RETRY_DELAY, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET"], ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("https://", adapter) session.mount("http://", adapter) session.headers.update({ "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" ) }) return session def get_model_filename(model_id: str) -> str: """ 从 model_id 生成 GGUF 文件名。 mradermacher/Qwen3-8B-xxx-i1-GGUF -> Qwen3-8B-xxx.i1-Q4_0.gguf QuantFactory/Apollo2-9B-GGUF -> Apollo2-9B.Q8_0.gguf """ base_name = model_id.split("/")[-1] if "_-_" in base_name: base_name = base_name.split("_-_")[-1] if base_name.lower().endswith("-gguf"): base_name = base_name[:-5] match = re.search(r"-i(\d+)$", base_name) if match: number = match.group(1) return base_name[:match.start()] + f".i{number}-Q4_0.gguf" return base_name + ".Q8_0.gguf" # ============================================================ # 5. Step 1:从 hf-mirror 获取组织模型 # ============================================================ def get_org_models(session: requests.Session, org_name: str) -> List[str]: models: List[str] = [] page = START_PAGE pages_seen = 0 while END_PAGE == 0 or page <= END_PAGE: if MAX_SCAN_PAGES and pages_seen >= MAX_SCAN_PAGES: break try: interruptible_sleep(random.uniform(*RANDOM_DELAY_RANGE)) params = { "author": org_name, "page": page, "perPage": PER_PAGE, "sort": "lastModified", "direction": "-1", } response = session.get(HF_API_URL, params=params, timeout=20) response.raise_for_status() data = response.json() if not data: print(f"第 {page} 页无数据,停止获取") break page_models = [] for item in data: model_id = item.get("modelId") if model_id: page_models.append(model_id) models.append(model_id) print(f"成功获取第 {page} 页,共 {len(page_models)} 个模型") pages_seen += 1 page += 1 except ShutdownRequested: raise except Exception as e: print(f"获取第 {page} 页失败: {e}") pages_seen += 1 page += 1 # 去重但保持顺序 seen = set() unique_models = [] for model in models: if model not in seen: unique_models.append(model) seen.add(model) return unique_models # ============================================================ # 6. Step 2:按目标 GGUF 文件大小筛选 # ============================================================ def extract_gguf_file_size( session: requests.Session, model_id: str, target_filename: str, ) -> float: """ 返回 GB。 找不到或失败返回 -1。 """ interruptible_sleep(REQUEST_DELAY + random.uniform(*RANDOM_DELAY_RANGE)) files_api_url = f"https://hf-mirror.com/api/models/{model_id}/tree/main" for retry in range(RETRY_TIMES + 1): try: response = session.get(files_api_url, timeout=20) response.raise_for_status() files_data = response.json() for file_item in files_data: if ( file_item.get("type") == "file" and file_item.get("path") == target_filename ): return file_item.get("size", 0) / (1024 ** 3) print(f"未找到文件: {model_id} -> {target_filename}") return -1 except requests.exceptions.HTTPError as e: status_code = getattr(e.response, "status_code", None) if status_code == 429 and retry < RETRY_TIMES: wait_time = RETRY_DELAY * (retry + 1) print( f"触发 HF 限流: {model_id},等待 {wait_time} 秒后重试 " f"({retry + 1}/{RETRY_TIMES})" ) interruptible_sleep(wait_time) continue print(f"提取文件大小失败: {model_id} -> {target_filename}: {e}") return -1 except Exception as e: print(f"提取文件大小失败: {model_id} -> {target_filename}: {e}") return -1 return -1 def filter_models_by_gguf_size( session: requests.Session, models: List[str], max_size_gb: float, ) -> List[str]: filtered: List[str] = [] for index, model in enumerate(models, start=1): target_file = get_model_filename(model) file_size = extract_gguf_file_size(session, model, target_file) prefix = f"[{index}/{len(models)}]" if file_size != -1 and file_size <= max_size_gb: filtered.append(model) print( f"{prefix} 符合大小条件: {model} -> {target_file} " f"({file_size:.2f} GB)" ) else: size_str = f"{file_size:.2f} GB" if file_size != -1 else "未知" print( f"{prefix} 不符合大小条件: {model} -> {target_file} " f"({size_str})" ) return filtered # ============================================================ # 7. Step 3:ModelHub 查重,过滤已入库模型 # ============================================================ def check_model_in_modelhub_db(model_id: str) -> Optional[bool]: """ 返回: True = 已入库 False = 未入库 None = 查询异常 """ url = BASE_URL + MODELHUB_DB_CHECK_ENDPOINT payload = { "current": 1, "pageSize": 20, "searchText": model_id, } try: response = requests.post( url, json=payload, headers=BASE_HEADERS, timeout=15, ) if response.status_code != 200: print(f"查重 HTTP 异常: {model_id} -> {response.status_code}") return None resp_json = response.json() code = resp_json.get("code") records = resp_json.get("data", {}).get("records", []) if code != 0: print( f"查重接口业务异常: {model_id} -> " f"{resp_json.get('message', 'unknown')}" ) return None if len(records) == 0: print(f"未入库: {model_id}") return False first_id = records[0].get("id") print(f"已入库: {model_id},记录 ID: {first_id}") return True except requests.exceptions.Timeout: print(f"查重超时: {model_id}") return None except requests.exceptions.RequestException as e: print(f"查重网络错误: {model_id}: {e}") return None except Exception as e: print(f"查重未知错误: {model_id}: {e}") return None def filter_models_not_in_db(models: List[str]) -> List[str]: not_in_db: List[str] = [] for index, model in enumerate(models, start=1): print(f"[{index}/{len(models)}] 查重: {model}") result = check_model_in_modelhub_db(model) if result is False: not_in_db.append(model) elif result is True: pass else: # 查询异常时,默认不提交,避免重复入库。 print(f"查重异常,跳过提交: {model}") interruptible_sleep(0.3) return not_in_db # ============================================================ # 8. 登录 # ============================================================ def login() -> str: check_shutdown() if not USER_ACCOUNT or not USER_PASSWORD: raise RuntimeError( "没有设置账号密码,请在脚本中填写 USER_ACCOUNT / USER_PASSWORD。" ) payload = { "userAccount": USER_ACCOUNT, "userPassword": USER_PASSWORD, } print("正在登录 ModelHub...") resp = requests.post( BASE_URL + LOGIN_ENDPOINT, headers=BASE_HEADERS, json=payload, timeout=20, ) if resp.status_code != 200: raise RuntimeError(f"HTTP 登录失败: {resp.status_code} - {resp.text}") data = resp.json() if data.get("code") != 0: raise RuntimeError(f"业务登录失败: {data.get('message') or data}") token = data["data"]["token"] print("登录成功") return token def get_token() -> str: return login() # ============================================================ # 9. Step 4:提交验证任务 # ============================================================ def is_i_variant_model(model_filename: str) -> bool: return re.search(r"\.i\d+-Q4_0\.gguf$", model_filename) is not None def build_config_params(model_id: str) -> Tuple[str, str]: model_filename = get_model_filename(model_id) if FORCE_TARGET_GPU: target_gpu = FORCE_TARGET_GPU elif is_i_variant_model(model_filename): target_gpu = "Ascend_910-b4" else: target_gpu = "Mthreads_s4000" if target_gpu == "Ascend_910-b4": config_params = f"""framework: llamacpp api: chat lang: zh max_model_len: 4096 max_tokens: 1024 temperature: 0.7 repetition_penalty: 1.1 top_p: 0.9 sut_config: gpu_num: 1 values: command: ['/workspace/llama.cpp/build_ascend/bin/llama-server', '--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '16', '--n-gpu-layers', '128', '--prio', '3', '--min_p', '0.01', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '3316', '--jinja', '--flash-attn', 'off'] ref_config: gpu_num: 1 values: command: ['/workspace/llama.cpp/build/bin/llama-server', '--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '16', '--n-gpu-layers', '128', '--prio', '3', '--min_p', '0.01', '--ctx-size', '4096', '--host', '0.0.0.0', '--port', '80', '--jinja'] """ elif target_gpu == "Mthreads_s4000": config_params = f"""framework: llamacpp api: completion max_tokens: 1024 temperature: 0 repetition_penalty: 1.1 top_p: 0.9 max_model_len: 4096 sut_config: gpu_num: 1 values: command: ['/app/llama-server', '--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '20', '--n-gpu-layers', '999', '--prio', '3', '--min_p', '0.01', '--ctx-size', '2048', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off'] ref_config: gpu_num: 1 values: command: ['/workspace/llama.cpp/build/bin/llama-server', '--model', '/model/{model_filename}', '--alias', 'llm', '--threads', '20', '--n-gpu-layers', '999', '--prio', '3', '--min_p', '0.01', '--ctx-size', '2048', '--host', '0.0.0.0', '--port', '8000', '--jinja', '--flash-attn', 'off'] """ else: raise ValueError(f"暂不支持的 targetGpu: {target_gpu}") return target_gpu, config_params def is_business_success(data: Dict) -> bool: if not isinstance(data, dict): return False code = data.get("code") success = data.get("success") if code in (0, "0", 200, "200"): return True if success is True: return True return False def submit_adapt_task(token: str, model_id: str) -> str: """ 返回: SUCCESS LIMIT FAILED AUTH_FAILED """ check_shutdown() target_gpu, config_params = build_config_params(model_id) headers = { **BASE_HEADERS, "Authorization": f"Bearer {token}", } payload = { "modelAddress": model_id, "strategyId": STRATEGY_ID, "taskType": TASK_TYPE, "targetGpu": target_gpu, "framework": FRAMEWORK, "configParams": config_params, } print(f"\n提交模型: {model_id}") print(f"GPU: {target_gpu}") print(f"模型文件名: {get_model_filename(model_id)}") if DEBUG_PRINT_PAYLOAD: print(json.dumps(payload, ensure_ascii=False, indent=2)) try: resp = requests.post( BASE_URL + ADD_ADAPT_TASK_ENDPOINT, headers=headers, json=payload, timeout=20, ) print(f"HTTP 状态码: {resp.status_code}") if resp.status_code in (401, 403): print(f"鉴权失败: {resp.text}") return "AUTH_FAILED" try: data = resp.json() except Exception: data = None if resp.status_code != 200: print(f"提交失败,HTTP 错误: {resp.status_code} - {resp.text}") return "FAILED" if data is None: print(f"提交完成,但返回不是 JSON: {resp.text}") return "SUCCESS" if is_business_success(data): task_id = ( data.get("data", {}).get("taskId") or data.get("data", {}).get("id") or data.get("taskId") or data.get("id") ) if task_id: print(f"提交成功,Task ID: {task_id}") else: print(f"提交成功,返回: {json.dumps(data, ensure_ascii=False)}") return "SUCCESS" code = data.get("code") message = data.get("message", "") if str(code) == str(TASK_LIMIT_CODE): print(f"达到任务上限,稍后重试: {json.dumps(data, ensure_ascii=False)}") return "LIMIT" if "数量已达上限" in message or "当前等待中或运行中" in message: print(f"达到任务上限,稍后重试: {json.dumps(data, ensure_ascii=False)}") return "LIMIT" print(f"提交失败,业务返回: {json.dumps(data, ensure_ascii=False)}") return "FAILED" except requests.RequestException as e: print(f"请求异常: {e}") return "FAILED" def submit_adapt_task_with_polling(token: str, model_id: str) -> Tuple[bool, str]: retry_times = 0 current_token = token while True: result = submit_adapt_task(current_token, model_id) if result == "SUCCESS": return True, current_token if result == "AUTH_FAILED": print("尝试重新登录后重试当前模型...") current_token = login() interruptible_sleep(2) continue if result == "FAILED": return False, current_token if result == "LIMIT": retry_times += 1 if MAX_LIMIT_RETRY_TIMES is not None and retry_times > MAX_LIMIT_RETRY_TIMES: print(f"达到最大等待次数,放弃当前模型: {model_id}") return False, current_token print( f"平台等待中/运行中的异步验证任务已满。" f"等待 {POLL_INTERVAL_SECONDS} 秒后重试当前模型。" f"模型: {model_id},等待次数: {retry_times}" ) interruptible_sleep(POLL_INTERVAL_SECONDS) continue print(f"未知提交状态: {result}") return False, current_token def iter_org_models(session: requests.Session, org_name: str): """逐页获取模型,避免等待全量列表完成后才开始处理。""" page = START_PAGE pages_seen = 0 while END_PAGE == 0 or page <= END_PAGE: if MAX_SCAN_PAGES and pages_seen >= MAX_SCAN_PAGES: return check_shutdown() try: interruptible_sleep(random.uniform(*RANDOM_DELAY_RANGE)) response = session.get( HF_API_URL, params={ "author": org_name, "page": page, "perPage": PER_PAGE, "sort": "lastModified", "direction": "-1", }, timeout=20, ) response.raise_for_status() data = response.json() if not data: return page_models = [item.get("modelId") for item in data if item.get("modelId")] print(f"扫描第 {page} 页,共 {len(page_models)} 个模型") for model_id in page_models: yield model_id pages_seen += 1 page += 1 except ShutdownRequested: raise except Exception as exc: print(f"扫描第 {page} 页失败: {exc}") pages_seen += 1 page += 1 def continuous_main() -> None: """持续扫描新模型,并由独立消费者逐个提交适配任务。""" ensure_output_dir() pending: "queue.Queue[str]" = queue.Queue() submitted = load_model_set(SUBMITTED_FILE) failed = load_model_set(FAILED_FILE) scheduled: Set[str] = set() stats = {"scanned": 0, "queued": 0, "submitted": 0, "failed": 0} stats_lock = threading.Lock() def submit_worker() -> None: token: Optional[str] = None while not stop_event_is_set(): try: model_id = pending.get(timeout=1) except queue.Empty: continue try: if token is None: token = get_token() ok, token = submit_adapt_task_with_polling(token, model_id) if ok: submitted.add(model_id) append_model(SUBMITTED_FILE, model_id) with stats_lock: stats["submitted"] += 1 else: failed.add(model_id) append_model(FAILED_FILE, model_id) with stats_lock: stats["failed"] += 1 except ShutdownRequested: return except Exception as exc: print(f"提交线程异常 {model_id}: {exc}") failed.add(model_id) append_model(FAILED_FILE, model_id) with stats_lock: stats["failed"] += 1 finally: scheduled.discard(model_id) pending.task_done() worker = threading.Thread(target=submit_worker, name="modelhub-submitter", daemon=True) worker.start() try: while not stop_event_is_set(): session = create_hf_session() try: for model_id in iter_org_models(session, ORG_NAME): check_shutdown() with stats_lock: stats["scanned"] += 1 if model_id in submitted or model_id in scheduled: continue if model_id in failed and not RETRY_FAILED: continue target_filename = get_model_filename(model_id) size_gb = extract_gguf_file_size(session, model_id, target_filename) if size_gb < 0 or size_gb > MAX_FILE_SIZE_GB: continue in_db = check_model_in_modelhub_db(model_id) if in_db is not False: continue scheduled.add(model_id) pending.put(model_id) with stats_lock: stats["queued"] += 1 print(f"已加入提交队列: {model_id} (队列长度 {pending.qsize()})") finally: session.close() print(f"扫描周期完成: {stats}; 下一次扫描等待 {SCAN_INTERVAL_SECONDS} 秒") interruptible_sleep(SCAN_INTERVAL_SECONDS) except ShutdownRequested: pass finally: request_shutdown() worker.join(timeout=SHUTDOWN_TIMEOUT_SECONDS) def stop_event_is_set() -> bool: return _shutdown_event.is_set() # ============================================================ # 10. 主流程:筛选 -> 查重 -> 提交 # ============================================================ def main() -> None: if PIPELINE_MODE not in {"continuous", "batch"}: raise ValueError("PIPELINE_MODE 必须是 continuous 或 batch") if PIPELINE_MODE == "continuous": continuous_main() return ensure_output_dir() args = parse_args() check_shutdown() if args.reset_submitted: remove_file_if_exists(SUBMITTED_FILE) if args.reset_failed: remove_file_if_exists(FAILED_FILE) print("=" * 80) print("Auto HF -> ModelHub Pipeline") print("=" * 80) print(f"组织: {ORG_NAME}") print(f"页码范围: {START_PAGE} - {END_PAGE}") print(f"每页数量: {PER_PAGE}") print(f"最大 GGUF 文件大小: {MAX_FILE_SIZE_GB} GB") print(f"输出目录: {OUTPUT_DIR}") print(f"运行模式: {args.run_mode}") print(f"起始步骤: Step {args.start_step}") print(f"强制重跑步骤: {sorted(args.force_rerun_steps) if args.force_rerun_steps else '无'}") print(f"断点续跑跳过已提交模型: {RESUME}") print("=" * 80) hf_session: Optional[requests.Session] = None try: # Step 1: 拉取 HF 模型 print("\nStep 1/4: 获取 HF / hf-mirror 模型列表") if should_run_step(1, ALL_MODELS_FILE, args): hf_session = hf_session or create_hf_session() all_models = get_org_models(hf_session, ORG_NAME) write_model_list(ALL_MODELS_FILE, all_models) print(f"共获取到 {len(all_models)} 个模型") print(f"已保存: {ALL_MODELS_FILE}") else: all_models = load_model_list(ALL_MODELS_FILE) print_step_loaded(1, ALL_MODELS_FILE, all_models) if not all_models: print("没有获取到模型,结束") return # Step 2: 按文件大小筛选 print("\nStep 2/4: 按 GGUF 文件大小筛选") if should_run_step(2, SIZE_FILTERED_FILE, args): hf_session = hf_session or create_hf_session() size_filtered_models = filter_models_by_gguf_size( hf_session, all_models, MAX_FILE_SIZE_GB, ) write_model_list(SIZE_FILTERED_FILE, size_filtered_models) print(f"大小筛选后剩余 {len(size_filtered_models)} 个模型") print(f"已保存: {SIZE_FILTERED_FILE}") else: size_filtered_models = load_model_list(SIZE_FILTERED_FILE) print_step_loaded(2, SIZE_FILTERED_FILE, size_filtered_models) if not size_filtered_models: print("没有符合大小条件的模型,结束") return # Step 3: ModelHub 查重 print("\nStep 3/4: 查询 ModelHub 是否已入库") if should_run_step(3, NOT_IN_DB_FILE, args): not_in_db_models = filter_models_not_in_db(size_filtered_models) write_model_list(NOT_IN_DB_FILE, not_in_db_models) print(f"未入库模型共 {len(not_in_db_models)} 个") print(f"已保存: {NOT_IN_DB_FILE}") else: not_in_db_models = load_model_list(NOT_IN_DB_FILE) print_step_loaded(3, NOT_IN_DB_FILE, not_in_db_models) if not not_in_db_models: print("没有需要提交的未入库模型,结束") return # Step 4: 登录并提交 print("\nStep 4/4: 登录并提交验证任务") token = get_token() already_submitted = load_model_set(SUBMITTED_FILE) if RESUME else set() already_failed = load_model_set(FAILED_FILE) if (RESUME and args.skip_failed) else set() skip_models = set(already_submitted) | set(already_failed) submit_candidates = [ model for model in not_in_db_models if model not in skip_models ] if RESUME: print(f"断点续跑开启,已成功提交过 {len(already_submitted)} 个模型") if args.skip_failed: print(f"本次会额外跳过历史失败模型 {len(already_failed)} 个") print(f"本次待提交 {len(submit_candidates)} 个模型") success_models: List[str] = [] failed_models: List[str] = [] for index, model_id in enumerate(submit_candidates, start=1): print("\n" + "=" * 80) print(f"[{index}/{len(submit_candidates)}] 准备提交: {model_id}") ok, token = submit_adapt_task_with_polling(token, model_id) if ok: success_models.append(model_id) append_model(SUBMITTED_FILE, model_id) else: failed_models.append(model_id) append_model(FAILED_FILE, model_id) if index < len(submit_candidates): interruptible_sleep(SUBMIT_INTERVAL_SECONDS) summary = { "org_name": ORG_NAME, "start_page": START_PAGE, "end_page": END_PAGE, "max_file_size_gb": MAX_FILE_SIZE_GB, "run_mode": args.run_mode, "start_step": args.start_step, "force_rerun_steps": sorted(args.force_rerun_steps), "all_models_count": len(all_models), "size_filtered_count": len(size_filtered_models), "not_in_db_count": len(not_in_db_models), "already_submitted_count": len(already_submitted), "submitted_this_run_count": len(success_models), "failed_this_run_count": len(failed_models), "success_models": success_models, "failed_models": failed_models, } with open(SUMMARY_FILE, "w", encoding="utf-8") as f: json.dump(summary, f, ensure_ascii=False, indent=2) print("\n" + "=" * 80) print("流程完成") print(f"HF 获取模型数: {len(all_models)}") print(f"大小筛选后: {len(size_filtered_models)}") print(f"未入库模型数: {len(not_in_db_models)}") print(f"历史已提交跳过: {len(already_submitted)}") print(f"本次提交成功: {len(success_models)}") print(f"本次提交失败: {len(failed_models)}") print(f"汇总文件: {SUMMARY_FILE}") if failed_models: print("\n失败模型:") for model in failed_models: print(model) finally: if hf_session is not None: hf_session.close() if __name__ == "__main__": main()