feat: 完全離線化 Dockerfile v0.1.16

## 主要修復
- MinerU: 改用 system-level 安裝 (uv pip install --system)
- MinerU: 模型下載到固定目錄 /opt/mineru/models
- BabelDOC: 完整 warmup + 離線 cache
- PDFMathTranslate: ONNX 模型使用 COPY 指令
- ImageMagick: 確保已安裝

## 結構優化
- Multi-Stage Build (8 stages)
- 使用 heredoc 語法處理多行 Python
- 分階段安裝避免記憶體尖峰

## Runtime 離線模式
- HF_HOME=/nonexistent
- HF_HUB_OFFLINE=1
- BABELDOC_OFFLINE=1
- PIP_NO_INDEX=1

## 新增驗證腳本
- scripts/verify-offline.sh
This commit is contained in:
Your Name 2026-01-25 11:07:03 +08:00
parent b23d42a7b1
commit 367e0d3056
4 changed files with 1682 additions and 761 deletions

310
scripts/verify-offline.sh Normal file
View file

@ -0,0 +1,310 @@
#!/bin/bash
# ==============================================================================
# ConvertX-CN 離線驗證腳本 v2.0
# ==============================================================================
#
# 用途:驗證 Docker Image 在完全離線環境下可正常運作
# 執行方式:
# 1. 容器內執行:/app/scripts/verify-offline.sh
# 2. 網路隔離測試docker run --network=none <image> /app/scripts/verify-offline.sh
#
# 驗證項目:
# 1. MinerU可執行 + 模型存在 + 配置正確
# 2. BabelDOC可執行 + cache 存在
# 3. PDFMathTranslate (pdf2zh):可執行 + ONNX 模型存在
# 4. ImageMagick可執行
# 5. 字型:自訂字型已安裝
# 6. 環境變數:離線模式已正確設定
#
# ==============================================================================
set -e
echo ""
echo "╔══════════════════════════════════════════════════════════════════════╗"
echo "║ ConvertX-CN 離線驗證腳本 v2.0 ║"
echo "║ Runtime Zero-Download Verification ║"
echo "╚══════════════════════════════════════════════════════════════════════╝"
echo ""
# 計數器
PASS=0
FAIL=0
WARN=0
ARCH=$(uname -m)
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 檢查函數
pass() {
echo -e "${GREEN}$1${NC}"
((PASS++))
}
fail() {
echo -e "${RED}$1${NC}"
((FAIL++))
}
warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
((WARN++))
}
info() {
echo -e " $1"
}
check_command() {
local cmd="$1"
local name="$2"
if command -v "$cmd" >/dev/null 2>&1; then
pass "$name 可執行: $(which $cmd)"
return 0
else
fail "$name 未安裝或不在 PATH"
return 1
fi
}
check_file() {
local path="$1"
local name="$2"
if [ -f "$path" ]; then
local size=$(ls -lh "$path" 2>/dev/null | awk '{print $5}')
pass "$name: $path ($size)"
return 0
else
fail "$name: $path 不存在"
return 1
fi
}
check_dir() {
local path="$1"
local name="$2"
local required="${3:-false}"
if [ -d "$path" ]; then
local size=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
pass "$name: $path ($size)"
return 0
else
if [ "$required" = "true" ]; then
fail "$name: $path 不存在(必要)"
else
warn "$name: $path 不存在(可選)"
fi
return 1
fi
}
check_env() {
local var="$1"
local expected="$2"
local actual="${!var}"
if [ -n "$actual" ]; then
if [ -n "$expected" ] && [ "$actual" != "$expected" ]; then
warn "$var = $actual(預期: $expected"
else
pass "$var = $actual"
fi
else
warn "$var 未設定"
fi
}
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 1. MinerU 驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
if [ "$ARCH" = "aarch64" ]; then
warn "ARM64 架構MinerU 不支援,跳過驗證"
else
# 檢查 mineru 命令
check_command "mineru" "MinerU CLI"
# 檢查模型目錄
MINERU_MODELS="/opt/mineru/models/PDF-Extract-Kit-1.0"
check_dir "$MINERU_MODELS" "MinerU Pipeline 模型" "true"
# 檢查配置檔
if check_file "/root/mineru.json" "MinerU 配置檔"; then
info "配置內容:"
cat /root/mineru.json 2>/dev/null | head -10
fi
# 驗證配置指向正確目錄
if [ -f "/root/mineru.json" ]; then
CONFIGURED_PATH=$(python3 -c "import json; f=open('/root/mineru.json'); d=json.load(f); print(d.get('models-dir',{}).get('pipeline',''))" 2>/dev/null || echo "")
if [ "$CONFIGURED_PATH" = "$MINERU_MODELS" ]; then
pass "mineru.json 指向正確目錄"
else
fail "mineru.json 指向錯誤目錄: $CONFIGURED_PATH"
fi
fi
fi
echo ""
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 2. BabelDOC 驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
check_command "babeldoc" "BabelDOC CLI"
# 檢查 cache 目錄
check_dir "/root/.cache/babeldoc" "BabelDOC Cache"
check_dir "/root/.cache/babeldoc/models" "BabelDOC Models"
check_dir "/root/.cache/babeldoc/fonts" "BabelDOC Fonts"
# 檢查 ONNX 模型
BABELDOC_ONNX="/root/.cache/babeldoc/models/doclayout_yolo_docstructbench_imgsz1024.onnx"
check_file "$BABELDOC_ONNX" "DocLayout-YOLO ONNX"
# 測試 babeldoc --help不應觸發下載
if command -v babeldoc >/dev/null 2>&1; then
if timeout 10 babeldoc --help >/dev/null 2>&1; then
pass "babeldoc --help 可離線執行"
else
warn "babeldoc --help 執行異常"
fi
fi
echo ""
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 3. PDFMathTranslate (pdf2zh) 驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
check_command "pdf2zh" "pdf2zh CLI"
# 測試 pdf2zh --help
if command -v pdf2zh >/dev/null 2>&1; then
if timeout 10 pdf2zh --help >/dev/null 2>&1; then
pass "pdf2zh --help 可離線執行"
else
warn "pdf2zh --help 執行異常"
fi
fi
echo ""
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 4. ImageMagick 驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
if check_command "convert" "ImageMagick (convert)"; then
VERSION=$(convert --version 2>/dev/null | head -1)
info "版本: $VERSION"
fi
check_command "identify" "ImageMagick (identify)"
check_command "mogrify" "ImageMagick (mogrify)"
echo ""
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔤 5. 字型驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
CUSTOM_FONTS_DIR="/usr/share/fonts/truetype/custom"
if check_dir "$CUSTOM_FONTS_DIR" "自訂字型目錄"; then
FONT_COUNT=$(ls -1 "$CUSTOM_FONTS_DIR"/*.ttf 2>/dev/null | wc -l)
info "字型檔案數量: $FONT_COUNT"
ls -1 "$CUSTOM_FONTS_DIR"/*.ttf 2>/dev/null | while read f; do
info " - $(basename $f)"
done
fi
# 驗證必要字型
check_file "$CUSTOM_FONTS_DIR/GoNotoKurrent-Regular.ttf" "Noto 通用字型"
check_file "$CUSTOM_FONTS_DIR/SourceHanSerifCN-Regular.ttf" "思源宋體(簡體)"
check_file "$CUSTOM_FONTS_DIR/SourceHanSerifTW-Regular.ttf" "思源宋體(繁體)"
check_file "$CUSTOM_FONTS_DIR/SourceHanSerifJP-Regular.ttf" "思源宋體(日文)"
check_file "$CUSTOM_FONTS_DIR/SourceHanSerifKR-Regular.ttf" "思源宋體(韓文)"
echo ""
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔧 6. 離線環境變數驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
echo "📋 HuggingFace 離線設定:"
check_env "HF_HUB_OFFLINE" "1"
check_env "TRANSFORMERS_OFFLINE" "1"
check_env "HF_DATASETS_OFFLINE" "1"
check_env "HF_HOME" "/nonexistent"
check_env "TRANSFORMERS_CACHE" "/nonexistent"
echo ""
echo "📋 MinerU 設定:"
check_env "MINERU_MODEL_SOURCE" "local"
check_env "MINERU_CONFIG"
echo ""
echo "📋 BabelDOC 設定:"
check_env "BABELDOC_OFFLINE" "1"
check_env "BABELDOC_CACHE_PATH"
echo ""
echo "📋 pip 設定:"
check_env "PIP_NO_INDEX" "1"
echo ""
# ==============================================================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔧 7. 其他核心工具驗證"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ==============================================================================
check_command "ffmpeg" "FFmpeg"
check_command "pandoc" "Pandoc"
check_command "libreoffice" "LibreOffice"
check_command "tesseract" "Tesseract OCR"
check_command "gs" "Ghostscript"
check_command "inkscape" "Inkscape"
check_command "markitdown" "MarkItDown"
echo ""
# ==============================================================================
echo ""
echo "╔══════════════════════════════════════════════════════════════════════╗"
echo "║ 驗證結果總結 ║"
echo "╚══════════════════════════════════════════════════════════════════════╝"
echo ""
echo " ✅ 通過: $PASS"
echo " ⚠️ 警告: $WARN"
echo " ❌ 失敗: $FAIL"
echo ""
if [ $FAIL -gt 0 ]; then
echo -e "${RED}╔══════════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ ❌ 離線驗證失敗! ║${NC}"
echo -e "${RED}║ 部分必要組件缺失Image 不應發布到 Production。 ║${NC}"
echo -e "${RED}╚══════════════════════════════════════════════════════════════════════╝${NC}"
exit 1
elif [ $WARN -gt 0 ]; then
echo -e "${YELLOW}╔══════════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ ⚠️ 離線驗證通過,但有警告 ║${NC}"
echo -e "${YELLOW}║ 部分可選功能可能受限,請確認是否符合需求。 ║${NC}"
echo -e "${YELLOW}╚══════════════════════════════════════════════════════════════════════╝${NC}"
exit 0
else
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ 離線驗證完全通過! ║${NC}"
echo -e "${GREEN}║ 系統可在 docker run --network=none 環境正常運作。 ║${NC}"
echo -e "${GREEN}║ (翻譯服務除外,需網路連接) ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════════════╝${NC}"
exit 0
fi