# syntax=docker/dockerfile:1.7
# audio2text — 一份 Dockerfile，CPU(dev) / GPU(prod) 双形态。
#   docker build --build-arg VARIANT=cpu -t audio2text:cpu .
#   docker build --build-arg VARIANT=gpu -t audio2text:gpu .
#   docker compose --profile dev up -d --build      # 开发：dev target + 源码挂载 + reload
#
# 分层目标：依赖层(deps)稳定固化，代码层(final/dev)在最末，改代码不重装依赖。
# BuildKit 缓存挂载(--mount=type=cache)：pip wheel 跨构建复用，二次构建秒级。
#
# 安全约束：PyTorch CPU wheel 与 GPU(CUDA) wheel 是两个不兼容二进制包。
#   - CPU 版 torch.cuda.is_available()=False（CPU 预期）
#   - GPU 版 torch.cuda.is_available()=True（GPU 可调度）
# 因此 torch 必须按 VARIANT 分叉装不同 wheel，绝不能跨 variant 共享依赖层。
# deps 阶段用 FROM base-${VARIANT}，CPU/GPU 是两条独立构建链，各自装对应 torch。

ARG VARIANT=cpu

# ---------------------------------------------------------------------------
# 1. 基础镜像分叉：CPU 用 slim Python，GPU 用 CUDA runtime + 手动装 python3.12
# ---------------------------------------------------------------------------
FROM python:3.12-slim AS base-cpu

FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base-gpu
# Ubuntu 22.04 默认源只有 python3.10/3.11，需加 deadsnakes PPA 才能装 python3.12。
# 注意：不能用 python3-pip（那是 3.10 的系统 pip，会把包装到 3.10 site-packages）。
# 用 python3.12 -m ensurepip 给 3.12 装 pip，确保所有包进 3.12 目录。
# DEBIAN_FRONTEND=noninteractive 避免 tzdata 等包进入交互式配置卡住构建。
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && apt-get install -y --no-install-recommends \
        software-properties-common gnupg ca-certificates \
    && add-apt-repository -y ppa:deadsnakes/ppa \
    && apt-get update -y && apt-get install -y --no-install-recommends \
        python3.12 python3.12-venv python3.12-dev \
        && rm -rf /var/lib/apt/lists/* \
    && ln -sf /usr/bin/python3.12 /usr/local/bin/python3 \
    && ln -sf /usr/bin/python3.12 /usr/local/bin/python \
    && python3 -m ensurepip \
    && python3 -m pip install --upgrade pip

# ---------------------------------------------------------------------------
# 2. deps：系统依赖 + Python 依赖（含 torch）。CPU/GPU 各自一条独立链。
#    final / dev 都 FROM deps，继承对应 variant 的 torch，不串。
# ---------------------------------------------------------------------------
FROM base-${VARIANT} AS deps
ARG VARIANT
ENV PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    HF_HOME=/models/huggingface \
    CT2_CACHE=/models/ctranslate2

# ffmpeg 是核心系统依赖；patchelf 用于修复 ctranslate2 可执行栈
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update -y && apt-get install -y --no-install-recommends \
        ffmpeg ca-certificates patchelf \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt /app/requirements.txt

# 【顺序关键】必须先装 torch（按 VARIANT 分叉），再装 requirements。
# 原因：requirements 里的 transformers / accelerate 依赖 torch，若先装 requirements，
# pip 会从默认 PyPI 拉来 GPU 版 torch + nvidia-* 全家桶（~2GB），即使后续覆盖装 CPU torch，
# 那些无用的 nvidia 包仍残留在镜像里。先装 torch 让 pip 解析 requirements 时 torch 已满足。
#
# CPU 走 cpu index（~200MB，无 nvidia 依赖）；GPU 走默认 PyPI（带 CUDA，~2.5GB）。
# 两套 wheel 二进制不兼容：CPU 版 cuda.is_available()=False，GPU 版 =True。不可共享。
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
    pip install --upgrade pip && \
    if [ "$VARIANT" = "cpu" ]; then \
        pip install torch --index-url https://download.pytorch.org/whl/cpu ; \
    else \
        pip install torch ; \
    fi

# torch 已就位，requirements 里的 transformers/accelerate 解析时复用已装 torch，不重复拉
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
    pip install -r /app/requirements.txt

# ctranslate2 的 .so 带可执行栈标志（PT_GNU_STACK X），在某些内核 + Docker 组合下
# 会触发 "cannot enable executable stack as shared object requires"。用 patchelf
# 清掉该标志（改为 RW），无需放宽容器安全策略。
# 注意：库在 ctranslate2.libs/ 隐藏目录（pip wheel 拆分产物），不在 ctranslate2/ 包目录。
RUN for d in /usr/local/lib/python3.12/site-packages/ctranslate2.libs \
             /usr/local/lib/python3.12/site-packages/ctranslate2; do \
        [ -d "$d" ] && find "$d" -name '*.so*' \
            -exec patchelf --clear-execstack {} \; 2>/dev/null || true; \
    done; \
    python -c "import ctranslate2; print('ctranslate2 stack fix verified', ctranslate2.__version__)"

# ---------------------------------------------------------------------------
# 3. dev：从 deps 继承依赖，不 COPY 代码（compose 用 volume 挂载源码）。
#    开启 uvicorn --reload，改代码零重建、保存即生效。
#    注意：dev 必须在 final 之前，保证 `docker build` 默认 target 是 final。
# ---------------------------------------------------------------------------
FROM deps AS dev
VOLUME ["/data", "/models"]
# ctranslate2/faster-whisper 用系统动态链接器找 cuDNN（不走 torch 的库加载），
# 需把 torch wheel 自带的 nvidia 库目录加入 LD_LIBRARY_PATH，否则 GPU 推理报
# "Unable to load libcudnn_ops.so.9"。放在 final/dev 而非 deps，避免 ENV 变化
# 导致 deps 的 apt/pip 层缓存失效。CPU 镜像无此目录，路径被忽略不影响。
ENV CONFIG_PATH=/app/config.yaml \
    LD_LIBRARY_PATH=/usr/local/lib/python3.12/site-packages/nvidia/cudnn/lib:/usr/local/lib/python3.12/dist-packages/nvidia/cudnn/lib:/usr/local/lib/python3.12/site-packages/nvidia/cublas/lib:/usr/local/lib/python3.12/dist-packages/nvidia/cublas/lib

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

# ---------------------------------------------------------------------------
# 4. final（prod）：从 deps 继承全部依赖，只加 app 代码。
#    放在最后 = `docker build` 默认 target。setup.sh / start.sh 依赖此默认行为。
# ---------------------------------------------------------------------------
FROM deps AS final
COPY app /app/app
COPY scripts /app/scripts
COPY config.example.yaml /app/config.example.yaml

# 运行时数据：上传 / 中间产物 / 输出字幕 / 模型缓存
# 全部走 volume，镜像本身无状态、无敏感数据
VOLUME ["/data", "/models"]
ENV CONFIG_PATH=/app/config.yaml \
    LD_LIBRARY_PATH=/usr/local/lib/python3.12/site-packages/nvidia/cudnn/lib:/usr/local/lib/python3.12/dist-packages/nvidia/cudnn/lib:/usr/local/lib/python3.12/site-packages/nvidia/cublas/lib:/usr/local/lib/python3.12/dist-packages/nvidia/cublas/lib

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
