refactor(docker): 完整重構 Dockerfile v0.1.16

BREAKING CHANGE: Dockerfile 結構重新設計

## 重構內容

### Multi-Stage Build 結構
- Stage 1 [base]: Bun runtime 基礎
- Stage 2 [install]: Node 依賴安裝
- Stage 3 [prerelease]: 應用程式建構
- Stage 4 [system-tools]: APT 系統工具(拆分為 14 個獨立 RUN)
- Stage 5 [fonts]: 字型安裝
- Stage 6 [python-tools]: Python CLI 工具(拆分為 9 個獨立 RUN)
- Stage 7 [models]: 模型下載
- Stage 8 [release]: 最終 Image

### 新增 Scripts
- install-system-tools.sh: 系統工具安裝
- install-fonts.sh: 字型安裝
- install-python-tools.sh: Python 工具安裝
- download-models.sh: 模型下載
- verify-installation.sh: 完整安裝驗證

### 刪除舊檔案
- Dockerfile.backup
- Dockerfile.offline
- Dockerfile.old

### 路徑更新
- MinerU 模型: /opt/convertx/models/mineru/PDF-Extract-Kit-1.0
- BabelDOC 快取: /root/.cache/babeldoc

### Multi-Arch 支援
- linux/amd64: 功能完整
- linux/arm64: 安全降級(MinerU/resvg 跳過)

Closes #docker-refactor
This commit is contained in:
Your Name 2026-01-25 13:15:31 +08:00
parent e43649bc96
commit 55b0f519ab
10 changed files with 1797 additions and 2008 deletions

306
scripts/download-models.sh Normal file
View file

@ -0,0 +1,306 @@
#!/usr/bin/env bash
# ==============================================================================
# ConvertX-CN 模型下載腳本
# ==============================================================================
# 用途:下載所有 AI 模型MinerU, BabelDOC, PDFMathTranslate
# 執行環境Docker build stage (models-download)
#
# ⚠️ 重要:此腳本在 build time 執行,下載所有模型到固定目錄
# Runtime 完全離線,不得有任何下載行為
# ==============================================================================
set -euo pipefail
# 顏色輸出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}📥 [INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}✅ [SUCCESS]${NC} $1"; }
log_warn() { echo -e "${YELLOW}⚠️ [WARN]${NC} $1"; }
log_error() { echo -e "${RED}❌ [ERROR]${NC} $1"; }
# 架構檢測
ARCH=$(uname -m)
log_info "檢測架構: ${ARCH}"
# ==============================================================================
# 目錄定義
# ==============================================================================
MODELS_BASE_DIR="/opt/convertx/models"
MINERU_MODELS_DIR="${MODELS_BASE_DIR}/mineru"
BABELDOC_CACHE_DIR="/root/.cache/babeldoc"
BABELDOC_MODELS_DIR="${BABELDOC_CACHE_DIR}/models"
BABELDOC_FONTS_DIR="${BABELDOC_CACHE_DIR}/fonts"
BABELDOC_CMAP_DIR="${BABELDOC_CACHE_DIR}/cmap"
BABELDOC_TIKTOKEN_DIR="${BABELDOC_CACHE_DIR}/tiktoken"
# ==============================================================================
# 創建目錄結構
# ==============================================================================
create_directories() {
log_info "創建模型目錄結構..."
mkdir -p "${MINERU_MODELS_DIR}"
mkdir -p "${BABELDOC_MODELS_DIR}"
mkdir -p "${BABELDOC_FONTS_DIR}"
mkdir -p "${BABELDOC_CMAP_DIR}"
mkdir -p "${BABELDOC_TIKTOKEN_DIR}"
log_success "目錄結構創建完成"
}
# ==============================================================================
# 下載 MinerU Pipeline 模型
# ==============================================================================
download_mineru_models() {
log_info "下載 MinerU Pipeline 模型..."
if [ "${ARCH}" = "aarch64" ]; then
log_warn "ARM64跳過 MinerU 模型下載(不支援)"
return 0
fi
python3 << 'PYTHON'
from huggingface_hub import snapshot_download
import os
models_dir = os.environ.get('MINERU_MODELS_DIR', '/opt/convertx/models/mineru')
print(f'下載 PDF-Extract-Kit-1.0 到 {models_dir}...')
try:
snapshot_download(
repo_id='opendatalab/PDF-Extract-Kit-1.0',
local_dir=f'{models_dir}/PDF-Extract-Kit-1.0',
local_dir_use_symlinks=False,
resume_download=True
)
print('✅ PDF-Extract-Kit-1.0 下載完成')
except Exception as e:
print(f'❌ 下載失敗: {e}')
raise
PYTHON
# 顯示模型大小
log_info "MinerU 模型目錄內容:"
du -sh "${MINERU_MODELS_DIR}/PDF-Extract-Kit-1.0" 2>/dev/null || true
ls -la "${MINERU_MODELS_DIR}/" 2>/dev/null || true
log_success "MinerU 模型下載完成"
}
# ==============================================================================
# 產生 MinerU 配置檔
# ==============================================================================
generate_mineru_config() {
log_info "產生 MinerU 配置檔..."
if [ "${ARCH}" = "aarch64" ]; then
log_warn "ARM64跳過 MinerU 配置"
return 0
fi
python3 << 'PYTHON'
import json
import os
mineru_models_dir = os.environ.get('MINERU_MODELS_DIR', '/opt/convertx/models/mineru')
config = {
'models-dir': {
'pipeline': f'{mineru_models_dir}/PDF-Extract-Kit-1.0',
'vlm': ''
},
'model-source': 'local',
'latex-delimiter-config': {
'display': {'left': '@@', 'right': '@@'},
'inline': {'left': '@', 'right': '@'}
}
}
# 寫入多個位置
config_paths = [
'/opt/convertx/mineru.json',
'/root/mineru.json'
]
for path in config_paths:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
json.dump(config, f, indent=2)
print(f'✅ 已產生: {path}')
print(json.dumps(config, indent=2))
PYTHON
log_success "MinerU 配置檔產生完成"
}
# ==============================================================================
# BabelDOC warmup下載所有資源
# ==============================================================================
babeldoc_warmup() {
log_info "執行 BabelDOC warmup..."
export BABELDOC_CACHE_PATH="${BABELDOC_CACHE_DIR}"
if command -v babeldoc >/dev/null 2>&1; then
babeldoc --warmup 2>&1 || {
log_warn "BabelDOC warmup 可能有警告,但繼續"
}
log_info "BabelDOC 快取內容:"
ls -la "${BABELDOC_CACHE_DIR}/" 2>/dev/null || true
du -sh "${BABELDOC_CACHE_DIR}/" 2>/dev/null || true
else
log_warn "babeldoc 不可用,跳過 warmup"
fi
log_success "BabelDOC warmup 完成"
}
# ==============================================================================
# 下載 tiktoken 編碼
# ==============================================================================
download_tiktoken() {
log_info "下載 tiktoken 編碼..."
python3 << 'PYTHON'
try:
import tiktoken
encodings = ['cl100k_base', 'p50k_base', 'r50k_base']
for enc_name in encodings:
try:
enc = tiktoken.get_encoding(enc_name)
print(f'✅ tiktoken {enc_name} 已下載')
except Exception as e:
print(f'⚠️ tiktoken {enc_name} 下載失敗: {e}')
except ImportError:
print('⚠️ tiktoken 未安裝,跳過')
PYTHON
log_success "tiktoken 編碼下載完成"
}
# ==============================================================================
# 複製預下載的 ONNX 模型(從 COPY 複製)
# ==============================================================================
setup_onnx_models() {
log_info "設定 ONNX 模型..."
# 如果有預下載的模型,複製到正確位置
local ONNX_MODEL="doclayout_yolo_docstructbench_imgsz1024.onnx"
local SRC_PATHS=(
"/app/models/${ONNX_MODEL}"
"/tmp/models/${ONNX_MODEL}"
)
for src in "${SRC_PATHS[@]}"; do
if [ -f "${src}" ]; then
cp "${src}" "${BABELDOC_MODELS_DIR}/"
log_success "已複製 ONNX 模型: ${src} -> ${BABELDOC_MODELS_DIR}/"
return 0
fi
done
log_warn "未找到預下載的 ONNX 模型,將依賴 babeldoc --warmup"
}
# ==============================================================================
# 驗證下載結果
# ==============================================================================
verify_downloads() {
log_info "驗證模型下載..."
echo ""
echo "模型目錄統計:"
echo "=============================================="
# MinerU
if [ "${ARCH}" != "aarch64" ]; then
if [ -d "${MINERU_MODELS_DIR}/PDF-Extract-Kit-1.0" ]; then
local size
size=$(du -sh "${MINERU_MODELS_DIR}/PDF-Extract-Kit-1.0" 2>/dev/null | awk '{print $1}')
echo " ✅ MinerU Pipeline: ${size}"
else
echo " ❌ MinerU Pipeline: 不存在"
fi
if [ -f "/root/mineru.json" ]; then
echo " ✅ MinerU 配置檔: 已產生"
else
echo " ❌ MinerU 配置檔: 不存在"
fi
else
echo " ⚠️ MinerU: ARM64 跳過"
fi
# BabelDOC
if [ -d "${BABELDOC_CACHE_DIR}" ]; then
local size
size=$(du -sh "${BABELDOC_CACHE_DIR}" 2>/dev/null | awk '{print $1}')
echo " ✅ BabelDOC 快取: ${size}"
else
echo " ⚠️ BabelDOC 快取: 不存在"
fi
# ONNX 模型
if [ -f "${BABELDOC_MODELS_DIR}/doclayout_yolo_docstructbench_imgsz1024.onnx" ]; then
local size
size=$(ls -lh "${BABELDOC_MODELS_DIR}/doclayout_yolo_docstructbench_imgsz1024.onnx" | awk '{print $5}')
echo " ✅ DocLayout-YOLO ONNX: ${size}"
else
echo " ⚠️ DocLayout-YOLO ONNX: 不存在"
fi
echo "=============================================="
log_success "驗證完成"
}
# ==============================================================================
# 清理下載快取
# ==============================================================================
cleanup_cache() {
log_info "清理下載快取..."
rm -rf /tmp/hf_download_cache 2>/dev/null || true
rm -rf /root/.cache/huggingface 2>/dev/null || true
rm -rf /root/.cache/pip 2>/dev/null || true
rm -rf /root/.cache/uv 2>/dev/null || true
# 清理 Python cache
find /usr -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
log_success "快取清理完成"
}
# ==============================================================================
# 主程式
# ==============================================================================
main() {
log_info "=========================================="
log_info "開始下載模型 (${ARCH})"
log_info "=========================================="
create_directories
download_mineru_models
generate_mineru_config
setup_onnx_models
babeldoc_warmup
download_tiktoken
verify_downloads
cleanup_cache
log_success "=========================================="
log_success "所有模型下載完成!"
log_success "=========================================="
}
# 執行
main "$@"