download_model.py 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. """Download and validate the configured local embedding model; never sends business data."""
  2. from __future__ import annotations
  3. import argparse
  4. import json
  5. import os
  6. from pathlib import Path
  7. import shutil
  8. import subprocess
  9. import sys
  10. from step2_graph_building.config import get_embedding_model_dir
  11. ROOT = Path(__file__).resolve().parents[1]
  12. def model_files_ready(path: Path) -> bool:
  13. required = ['config.json', 'modules.json', 'tokenizer.json', 'tokenizer_config.json']
  14. try:
  15. for name in required:
  16. if not (path / name).is_file() or (path / name).stat().st_size == 0:
  17. return False
  18. json.loads((path / name).read_text(encoding='utf-8'))
  19. modules = json.loads((path / 'modules.json').read_text(encoding='utf-8'))
  20. for module in modules:
  21. relative = module.get('path', '')
  22. if (relative and module.get('type') != 'sentence_transformers.models.Normalize'
  23. and not (path / relative / 'config.json').is_file()):
  24. return False
  25. index = path / 'model.safetensors.index.json'
  26. if index.is_file():
  27. weights = set(json.loads(index.read_text(encoding='utf-8'))['weight_map'].values())
  28. else:
  29. weights = {'model.safetensors'}
  30. return bool(weights) and all((path / name).is_file() and (path / name).stat().st_size > 0 for name in weights)
  31. except (OSError, ValueError, KeyError, TypeError, AttributeError):
  32. return False
  33. def verify_model(path: Path) -> None:
  34. import numpy as np
  35. from sentence_transformers import SentenceTransformer
  36. model = SentenceTransformer(str(path), device='cpu', local_files_only=True)
  37. vectors = model.encode(['部署检查'], normalize_embeddings=True, show_progress_bar=False)
  38. if len(vectors) != 1 or not np.isfinite(vectors).all() or np.linalg.norm(vectors) == 0:
  39. raise RuntimeError('本地模型编码验证失败')
  40. def ensure_model(*, check_only=False, force=False):
  41. path = get_embedding_model_dir()
  42. repo = os.getenv('EMBEDDING_MODEL_REPO', 'Qwen/Qwen3-Embedding-0.6B').strip()
  43. source = os.getenv('EMBEDDING_MODEL_SOURCE', 'modelscope').strip().lower()
  44. if not repo or source not in {'modelscope', 'huggingface'}:
  45. raise ValueError('请配置有效 EMBEDDING_MODEL_REPO,SOURCE 仅支持 modelscope/huggingface')
  46. if force or not model_files_ready(path):
  47. if check_only:
  48. raise RuntimeError(f'模型文件不完整: {path}')
  49. print(f'下载模型 {repo} → {path}({source})', flush=True)
  50. path.mkdir(parents=True, exist_ok=True)
  51. if source == 'modelscope':
  52. uv = shutil.which('uv')
  53. if not uv:
  54. raise RuntimeError('ModelScope 下载需要 uv;请先运行 sh deploy.sh setup')
  55. # ModelScope lives in an ephemeral uv environment, not project dependencies.
  56. code = 'from modelscope import snapshot_download; import sys; snapshot_download(sys.argv[1], local_dir=sys.argv[2])'
  57. subprocess.run([uv, 'run', '--frozen', '--with', 'modelscope', 'python', '-c', code, repo, str(path)], cwd=ROOT, check=True)
  58. else:
  59. from huggingface_hub import snapshot_download
  60. snapshot_download(repo_id=repo, local_dir=str(path), force_download=force)
  61. if not model_files_ready(path):
  62. raise RuntimeError(f'模型下载未完整,请检查网络后重试: {path}')
  63. else:
  64. print(f'模型文件已存在,跳过下载: {path}', flush=True)
  65. verify_model(path)
  66. print('本地模型加载和编码检查通过。', flush=True)
  67. def main():
  68. parser = argparse.ArgumentParser(description=__doc__)
  69. parser.add_argument('--check-only', action='store_true', help='仅校验本地模型,禁止下载')
  70. parser.add_argument('--force', action='store_true', help='重新调用下载器修复模型文件')
  71. args = parser.parse_args()
  72. if args.check_only and args.force:
  73. parser.error('--check-only 与 --force 不能同时使用')
  74. ensure_model(check_only=args.check_only, force=args.force)
  75. if __name__ == '__main__':
  76. main()