add offline embedding server & model downloader

This commit is contained in:
Haitao Pan 2025-08-10 16:22:53 +08:00
parent 98c3f12a62
commit 5fd3078e3c
7 changed files with 391 additions and 13 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
models/
hf_cache/
server/server/

154
docs/models_downloading.py Normal file
View File

@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
models_downloading.py
- 优先级CN 镜像 (hf-mirror.com) -> 代理(PROXY) -> 官方直连
- 统一缓存HF_HOME=./hf_cache可被环境变量覆盖
- 进度可见启用 huggingface_hub tqdm 进度
- 幂等安全本地目录已有关键文件则跳过下载
可选环境变量
- MODEL_ID 默认 "BAAI/bge-m3"
- MODEL_DIR 默认 "models/bge-m3"
- HF_HOME 默认 "./hf_cache"
- PROXY 默认 "socks5h://127.0.0.1:1081"留空表示不走代理
- HF_ENDPOINT 手动指定镜像时可设置脚本也会自动探测 cn mirror
"""
import os
import sys
from pathlib import Path
# ---------- 配置 ----------
MODEL_ID = os.getenv("MODEL_ID", "BAAI/bge-m3")
MODEL_DIR = Path(os.getenv("MODEL_DIR", "models/bge-m3"))
HF_HOME = Path(os.getenv("HF_HOME", Path.cwd() / "hf_cache"))
CN_MIRROR = "https://hf-mirror.com"
PROXY = os.getenv("PROXY", "socks5h://127.0.0.1:1081")
# ---------- 提前设置缓存目录(在 import 前) ----------
os.environ["HF_HOME"] = str(HF_HOME)
# ---------- 依赖安装 ----------
def _install(pkgs: str):
os.system(f"{sys.executable} -m pip install -U {pkgs}")
try:
import requests
except ImportError:
_install("requests")
import requests
# 若走 socks 代理需要 PySocks
if PROXY and "socks" in PROXY:
try:
import socks # noqa: F401
except ImportError:
_install("'requests[socks]'")
# ---------- 选择网络模式(镜像 → 代理 → 官方) ----------
def set_network_mode():
# 若外部已设置 HF_ENDPOINT尊重外部配置
if os.getenv("HF_ENDPOINT"):
print(f"🌏 Using custom HF endpoint: {os.getenv('HF_ENDPOINT')}")
return
# 1) 尝试 CN 镜像
try:
r = requests.get(CN_MIRROR, timeout=2)
if r.status_code == 200:
os.environ["HF_ENDPOINT"] = CN_MIRROR
print(f"🌏 Using Hugging Face CN mirror: {CN_MIRROR}")
return
except Exception:
pass
# 2) 走代理
if PROXY:
os.environ["HTTP_PROXY"] = PROXY
os.environ["HTTPS_PROXY"] = PROXY
print(f"🌐 Using proxy: {PROXY}")
return
# 3) 官方直连
print("⚠️ No mirror or proxy, using official huggingface.co")
set_network_mode()
# 现在再导入 huggingface_hub确保拿到正确的 endpoint/proxy 设置
try:
from huggingface_hub import snapshot_download
except ImportError:
_install("'huggingface_hub[tqdm]'")
from huggingface_hub import snapshot_download
# ---------- 工具函数 ----------
KEY_FILES = (
"tokenizer.json",
"config.json",
"sentencepiece.bpe.model",
"onnx/model.onnx",
"pytorch_model.bin",
"model.safetensors",
)
def has_local_model(root: Path) -> bool:
if not root.exists():
return False
for k in KEY_FILES:
if any(root.rglob(k)):
return True
# 兜底:只要非空也算有内容(对应部分仓库布局)
return any(root.iterdir())
# ---------- 主流程 ----------
def main():
print("⬇️ Downloading model from Hugging Face…")
print(f" Model ID : {MODEL_ID}")
print(f" Save dir : {MODEL_DIR}")
print(f" HF_HOME : {HF_HOME}")
if os.getenv("HF_ENDPOINT"):
print(f" Endpoint : {os.getenv('HF_ENDPOINT')}")
elif os.getenv("HTTP_PROXY"):
print(f" Proxy : {os.getenv('HTTP_PROXY')}")
else:
print(" Endpoint : official (huggingface.co)")
MODEL_DIR.parent.mkdir(parents=True, exist_ok=True)
HF_HOME.mkdir(parents=True, exist_ok=True)
# 已有可用文件 → 跳过下载
if has_local_model(MODEL_DIR):
print(f"📂 Local model exists, skip download: {MODEL_DIR}")
print("💡 To force re-download, remove the folder and rerun.")
return
# 下载(显示进度)
try:
snapshot_download(
repo_id=MODEL_ID,
local_dir=str(MODEL_DIR),
local_dir_use_symlinks=False,
tqdm_class=None, # 使用默认 tqdm 进度条
)
except Exception as e:
# 失败时检查是否已经有部分或全部文件
if has_local_model(MODEL_DIR):
print(f"⚠️ Online fetch failed but local files exist: {MODEL_DIR}")
print(f" Error: {e}")
else:
print("❌ Download failed and no local files found.")
print(f" Error: {e}")
print("🔁 Try: 1) 切换镜像/代理 2) 检查网络 3) 稍后重试")
sys.exit(1)
# 最终确认
if has_local_model(MODEL_DIR):
print(f"✅ Model cached to {MODEL_DIR}")
print("💡 To run offline later, set: export HF_HUB_OFFLINE=1")
else:
print("❌ No model files found after download attempt.")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
Offline Embedding Server (Flask + fastembed)
职责仅加载本地模型目录并提供 /v1/embeddings 服务
下载请使用 docs/models_downloading.py已含镜像/代理逻辑
环境变量可选
- MODEL_ID 默认 "BAAI/bge-m3"仅用于返回值展示
- BGE_M3_DIR 默认 "models/bge-m3"本地已下载的模型目录
- HF_HOME 默认 "./hf_cache"本地缓存离线可用
- EMBED_HOST 默认 "0.0.0.0"
- EMBED_PORT 默认 "9000"
"""
import os
import sys
import numpy as np
from pathlib import Path
# ---------------- 配置 ----------------
MODEL_ID = os.getenv("MODEL_ID", "BAAI/bge-m3")
MODEL_DIR = Path(os.getenv("BGE_M3_DIR", "models/bge-m3"))
HF_HOME = Path(os.getenv("HF_HOME", Path.cwd() / "hf_cache"))
HOST = os.getenv("EMBED_HOST", "0.0.0.0")
PORT = int(os.getenv("EMBED_PORT", 9000))
# 关键文件(用于判定目录是否可用)
KEY_FILES = ("tokenizer.json", "config.json", "sentencepiece.bpe.model")
# -------------- 依赖处理 --------------
def _pip_install(pkgs: str):
os.system(f"{sys.executable} -m pip install -U {pkgs}")
try:
from flask import Flask, request, jsonify
except ImportError:
_pip_install("flask")
from flask import Flask, request, jsonify
try:
from fastembed import TextEmbedding
except ImportError:
_pip_install("fastembed")
from fastembed import TextEmbedding
try:
import numpy as _np # 仅用于确认已装
except ImportError:
_pip_install("numpy")
import numpy as _np # noqa
# -------------- 校验本地模型 --------------
def has_local_model(p: Path) -> bool:
if not p.exists():
return False
# 任一关键文件存在即认为可用;或目录非空兜底
for k in KEY_FILES:
if any(p.rglob(k)):
return True
return any(p.iterdir())
if not has_local_model(MODEL_DIR):
print(f"❌ Model not found or incomplete in: {MODEL_DIR}")
print(" 请先执行下载python docs/models_downloading.py")
sys.exit(1)
# -------------- 设置离线运行 --------------
os.environ["HF_HOME"] = str(HF_HOME) # 统一缓存目录Mac/Linux 一致)
os.environ["HF_HUB_OFFLINE"] = "1" # 强制完全离线
HF_HOME.mkdir(parents=True, exist_ok=True)
# -------------- 启动服务 --------------
app = Flask(__name__)
model = TextEmbedding(str(MODEL_DIR)) # fastembed 直接从本地目录加载
@app.post("/v1/embeddings")
def embeddings():
data = request.get_json(force=True) or {}
inp = data.get("input", [])
if isinstance(inp, str):
texts = [inp]
elif isinstance(inp, list):
# 过滤保证都是字符串
texts = [str(x) for x in inp]
else:
return jsonify({"error": "invalid input type"}), 400
# fastembed 默认产出已归一向量;这里再 L2 保底
vecs = []
for v in model.embed(texts):
v = np.asarray(v, dtype=np.float32)
v = v / (np.linalg.norm(v) + 1e-12)
vecs.append(v.tolist())
return jsonify({
"object": "list",
"data": [
{"object": "embedding", "index": i, "embedding": e}
for i, e in enumerate(vecs)
],
"model": data.get("model", MODEL_ID),
})
@app.get("/healthz")
def healthz():
# 存在即健康
return "ok", 200
@app.get("/readyz")
def readyz():
# 模型已加载即就绪
try:
_ = model # 触发引用
return "ready", 200
except Exception as e:
return f"not ready: {e}", 503
if __name__ == "__main__":
print(f"🚀 Embedding server")
print(f" Model Dir : {MODEL_DIR}")
print(f" HF_HOME : {HF_HOME}")
print(f" Offline : {os.environ.get('HF_HUB_OFFLINE')}")
print(f" Listen on : http://{HOST}:{PORT}")
app.run(host=HOST, port=PORT)

View File

@ -136,3 +136,36 @@ make init-db
使用 Markdown 编写(支持标题、列表、代码块等)。
可使用 plantuml 或 mermaid 绘制架构图并嵌入 Markdown。
## DEV
1. 运行(首次会自动下载模型)
python offline_embed_server.py
2. 测试接口
编辑
curl -s http://127.0.0.1:9000/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"model":"BAAI/bge-m3","input":["你好","PGVector 怎么建 HNSW"]}' | jq .
3. 环境变量(可选)
export BGE_M3_DIR="/path/to/bge-m3"
export EMBED_HOST="127.0.0.1"
export EMBED_PORT=9100
python offline_embed_server.py
## Ollama API test
用流式接收(推荐):
curl http://127.0.0.1:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-oss:20b",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me three tips for optimizing HNSW in PostgreSQL."}
],
"max_tokens": 512,
"stream": true
}'
这样会实时输出分块数据

53
docs/setup_macos_m4.sh Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
echo "==> 1. Xcode Command Line Tools"
xcode-select -p >/dev/null 2>&1 || xcode-select --install || true
echo "==> 2. Homebrew"
if ! command -v brew >/dev/null 2>&1; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
echo "==> 3. 基础工具"
brew update
brew install git gh wget curl jq cmake pkg-config tree htop tmux
echo "==> 4. Go / Node / Yarn"
brew install go
# Node 推荐用 corepack 管理pnpm/yarn
brew install node
corepack enable || true
corepack prepare yarn@stable --activate || true
echo "==> 5. PostgreSQL + pgvector"
brew install postgresql@16
brew services start postgresql@16
# pgvector 扩展Homebrew 版已包含或单独提供)
brew install pgvector || true
echo "==> 6. Redis"
brew install redis
brew services start redis
echo "==> 7. Python 与虚拟环境"
brew install python@3.12
python3 -m venv ~/.venvs/xcontrol && source ~/.venvs/xcontrol/bin/activate
pip install -U pip wheel
echo "==> 8. RAG: fastembed + Flask做本地 /v1/embeddings"
pip install -U fastembed flask numpy huggingface_hub
echo "==> 9. 可选PyTorch + MPSApple GPU 加速,用于 Transformers 生成)"
# 官方 pip 已支持 MPS一般直接安装即可若失败可按官网指引重装
pip install -U torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu
echo "==> 10. 可选Ollama本地生成模型"
if ! command -v ollama >/dev/null 2>&1; then
curl -fsSL https://ollama.com/install.sh | sh
fi
echo "==> 完成 ✅ 请重新打开终端或执行:"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"'

View File

@ -19,22 +19,33 @@ sync:
repo:
proxy: socks5://127.0.0.1:1080 # 仅在同步仓库时使用代理
provider:
- name: allama
base_url: http://localhost:11434
token: ""
# For DEV
models:
embedder:
provider: "huggingface_hub"
models: "bge-m3"
endpoint: "http://127.0.0.1:9000/v1/embeddings"
generator:
provider: "ollama"
models:
- 'gpt-oss:20b'
- name: chutes
base_url: https://llm.chutes.ai
token: "cpk_xxxxxxxxxxxxxxxxxxxx"
models:
- 'moonshotai/Kimi-K2-Instruct'
endpoint: "http://127.0.0.1:11434/v1/chat/completions"
token: ""
# For PROD
#models:
# embedder:
#provider: "chutes"
#models: "bge-m3"
#endpoint: "https://chutes-baai-bge-m3.chutes.ai/embed/v1/embeddings"
#token: "cpk_xxxxxxxxxxxxxxxxxxxx"
# generator:
#provider: "chutes"
#endpoint: "https://llm.chutes.ai/v1/chat/completions"
#token: "cpk_xxxxxxxxxxxxxxxxxxxx"
#models:
# - 'moonshotai/Kimi-K2-Instruct'
embedding:
base_url: http://localhost:11434
token: ""
models: bge-m3
max_batch: 64
dimension: 1024 #维度
max_chars: 8000

2
ui/dist/index.html vendored

File diff suppressed because one or more lines are too long