#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" VENV_DIR="$ROOT_DIR/.venv" BACKEND_PORT="${BACKEND_PORT:-8000}" # ── find python ────────────────────────────────────────── PYTHON="" for cmd in python3.13 python3.12 python3.11 python3.10 python3.9 python3; do if command -v "$cmd" >/dev/null 2>&1; then PYTHON="$cmd" break fi done [[ -n "$PYTHON" ]] || { echo "[ERROR] 找不到 python3"; exit 1; } # verify version >= 3.9 if ! "$PYTHON" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3,9) else 1)'; then echo "[ERROR] 需要 Python 3.9+"; exit 1 fi # ── deps check & install ───────────────────────────────── DEPS_OK=0 if "$PYTHON" -c " import importlib.util, sys mods = ('fastapi','uvicorn','pandas','PIL','numpy','matplotlib','pydantic','openpyxl') for m in mods: if importlib.util.find_spec(m) is None: sys.exit(1) " 2>/dev/null; then DEPS_OK=1 fi if [[ "$DEPS_OK" == "0" ]]; then # try venv first if [[ -f "$VENV_DIR/bin/python" ]] && "$VENV_DIR/bin/python" --version >/dev/null 2>&1; then PYTHON="$VENV_DIR/bin/python" if "$PYTHON" -c " import importlib.util, sys for m in ('fastapi','uvicorn','pandas','PIL','numpy','matplotlib','pydantic','openpyxl'): if importlib.util.find_spec(m) is None: sys.exit(1) " 2>/dev/null; then DEPS_OK=1 fi fi fi if [[ "$DEPS_OK" == "0" ]]; then echo "[INFO] 安装依赖 (首次或缺失) ..." # create venv if needed if [[ ! -f "$VENV_DIR/bin/python" ]]; then "$PYTHON" -m venv "$VENV_DIR" fi PYTHON="$VENV_DIR/bin/python" "$PYTHON" -m pip install -q --upgrade pip "$PYTHON" -m pip install -q fastapi uvicorn python-multipart pydantic pandas openpyxl pillow numpy matplotlib fi # SciPy is an optional accelerator for line-spacing analysis. The production # requirements include it, but local dev environments may already have it in # the base interpreter while the project venv does not. Reuse that copy when # the Python ABI matches so startup does not require a network install. EXTRA_SCIPY_SITE="" if ! "$PYTHON" -c 'import scipy' >/dev/null 2>&1; then PYTHON_ABI=$("$PYTHON" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') for cmd in python3.13 python3.12 python3.11 python3.10 python3.9 python3; do if command -v "$cmd" >/dev/null 2>&1; then SCIPY_SITE=$("$cmd" -c ' import pathlib import scipy import sys expected = "'"$PYTHON_ABI"'" current = f"{sys.version_info.major}.{sys.version_info.minor}" print(pathlib.Path(scipy.__file__).parents[1] if current == expected else "") ' 2>/dev/null || true) if [[ -n "$SCIPY_SITE" ]]; then EXTRA_SCIPY_SITE="$SCIPY_SITE" echo "[OK] 使用外部 SciPy 加速线距计算: $SCIPY_SITE" break fi fi done fi # ── C++ extension ──────────────────────────────────────── EXT=$($PYTHON -c 'import importlib.machinery; print(importlib.machinery.EXTENSION_SUFFIXES[0])') EWC_SO="$ROOT_DIR/EfficientWordCloud/efficient_wordcloud/ewc_core${EXT}" EWC_CPP="$ROOT_DIR/EfficientWordCloud/efficient_wordcloud/src/ewc_core.cpp" if [[ ! -f "$EWC_SO" ]] || [[ "$EWC_CPP" -nt "$EWC_SO" ]]; then echo "[INFO] 编译 ewc_core ..." (cd "$ROOT_DIR/EfficientWordCloud" && "$PYTHON" setup.py build_ext --inplace >/dev/null) echo "[OK] 编译完成" else echo "[OK] ewc_core 就绪" fi # ── ensure runtime dirs ────────────────────────────────── mkdir -p "$ROOT_DIR/service_workspace" "$ROOT_DIR/service_assets" "$ROOT_DIR/service_projects" # ── port: auto-kill occupant ───────────────────────────── if ss -tln 2>/dev/null | grep -q ":$BACKEND_PORT "; then echo "[INFO] 端口 $BACKEND_PORT 被占用,正在释放 ..." fuser -k "$BACKEND_PORT/tcp" >/dev/null 2>&1 || true sleep 1 fi # ── start ──────────────────────────────────────────────── echo "[INFO] 启动后端 http://0.0.0.0:${BACKEND_PORT}" WORDCLOUD_SCIPY_SITE="$EXTRA_SCIPY_SITE" \ PYTHONPATH="$ROOT_DIR/EfficientWordCloud" \ "$PYTHON" -m uvicorn service.app:app \ --app-dir "$ROOT_DIR" \ --host 0.0.0.0 --port "$BACKEND_PORT" \ --reload & PID=$! echo "[OK] PID: $PID" echo "[OK] API: http://0.0.0.0:${BACKEND_PORT}/docs" trap 'echo; echo "[INFO] 停止 ..."; kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true; echo "[OK] 已停止"; exit 0' INT TERM wait "$PID"