# Runtime image for the whole app: the editor (admin), the static site server,
# the project homepage and umtool, plus the tools they shell out to (yt-dlp,
# ffmpeg, and a transcription backend).
#
# This is NOT one of the other two Dockerfiles and does not replace either:
#   Dockerfile.build — one hermetic container per SITE for the export build fan-out.
#   Dockerfile.test  — the sharded playwright e2e route.
# Both keep working exactly as they did; nothing here touches them.
#
# Three published targets, all built from the same app code:
#   runtime         CPU whisper.cpp.        The default.  (docker-compose.yml)
#   runtime-vulkan  parakeet.cpp on Vulkan. Any GPU with a Vulkan driver —
#                   AMD/RADV, Intel, NVIDIA.          (docker-compose.vulkan.yml)
#   runtime-cuda    whisper.cpp on CUDA.    NVIDIA only.  (docker-compose.gpu.yml)
#
# Used by docker-compose.yml. See RUNNING_IN_DOCKER.md.
#
# ---------------------------------------------------------------------------
# Why the whole repo ships instead of `output: "standalone"`
# ---------------------------------------------------------------------------
# editor/next.config.ts (and umtool/next.config.ts) explain it: the server does
# runtime-dynamic fs reads the file tracer cannot statically bound — readdir over
# a video dir, per-job logs, settings.json — so the .nft.json traces Next emits
# are deliberately never consumed. A standalone bundle built from them would be
# missing files nobody can enumerate ahead of time. So we ship the repo plus
# node_modules, exactly as Dockerfile.test already does. Read those comments
# before "optimising" this.

# ---------------------------------------------------------------------------
# Base images, and the one rule that ties them together
# ---------------------------------------------------------------------------
# GLIBC ONLY GOES FORWARD. A binary built against an older glibc runs on a newer
# one; the reverse fails at load time with "GLIBC_2.xx not found". The workspace
# (including native modules — lmdb, msgpackr-extract) is compiled ONCE in the
# `build` stage and copied into every runtime, so:
#
#   NODE_IMAGE must have the OLDEST glibc of anything it is copied into.
#
# bookworm is 2.36, trixie 2.41, ubuntu 24.04 2.39 — so building on bookworm and
# running on any of them is safe, and moving NODE_IMAGE to trixie would silently
# break the CUDA target. If you bump one of these, check that direction first.
#
# NODE'S MAJOR IS THE SECOND RULE, and it must be the same everywhere: the
# native modules are compiled against the build stage's Node ABI, so every
# runtime (NODE_IMAGE, RUNTIME_IMAGE here and in docker-compose.vulkan.yml,
# NODE_MAJOR in runtime-cuda) carries the same major. 22, not 20: the wrangler
# pinned in common/package.json refuses to start on anything older (its
# engines floor), and every deploy runs it from this image.
# common/publish/buildImage.test.ts holds all four to one major and that floor.
ARG NODE_IMAGE=node:22-bookworm-slim
# The runtime base, overridable per target: docker-compose.vulkan.yml builds with
# trixie because the Vulkan stack needs it (see the parakeet stage below).
ARG RUNTIME_IMAGE=node:22-bookworm-slim
# ubuntu24.04, not 22.04: 22.04 is glibc 2.35, OLDER than the bookworm the
# workspace is built on, and the native modules would not load.
ARG CUDA_DEVEL_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu24.04
ARG CUDA_RUNTIME_IMAGE=nvidia/cuda:12.6.3-runtime-ubuntu24.04
# whisper.cpp release to build. Pinned: this is a compiled dependency, and
# "whatever master was that day" is not a thing you can reproduce later.
ARG WHISPER_REF=v1.7.6
# parakeet.cpp, same reasoning. https://github.com/mudler/parakeet.cpp
ARG PARAKEET_REF=v0.5.0
# Compile parallelism for the transcription backends. Defaults to every core; lower it when the
# machine is shared or short on RAM (the CUDA build in particular runs one nvcc
# per job and each is hungry). `--build-arg WHISPER_BUILD_JOBS=4`.
ARG WHISPER_BUILD_JOBS=
# Which CUDA architectures the GPU build targets. Turing through Hopper, plus
# Pascal, so the image runs on more than the card it was built next to — at a
# real cost in build time, since every CUDA translation unit is compiled once per
# architecture. Narrow it (e.g. "86") when you are building for one known GPU.
#
# NOT "all-major", which is the obvious thing to write here and does not work:
# CMake only understands that value from 3.23, the CUDA base image ships CMake
# 3.22, and the failure is not a cmake error but
#   nvcc fatal : Unsupported gpu architecture 'compute_'
# i.e. the literal string reaching nvcc as if it were an architecture.
ARG CUDA_ARCHITECTURES="61;70;75;80;86;89;90"
# ggml compiles the CPU backend with -march=native by default, which bakes THIS
# machine's instruction set into the image. That is right for the normal case —
# you build on the box you run on, and it is measurably faster — and wrong the
# moment the image moves: an older CPU dies with SIGILL on the first tensor op.
# Set GGML_NATIVE=OFF when building an image you intend to publish or run
# elsewhere.
ARG GGML_NATIVE=ON

# ---------------------------------------------------------------------------
# whisper-cpu — build whisper-cli, the default transcription backend.
#
# Statically linked (BUILD_SHARED_LIBS=OFF) so the runtime stage copies ONE file
# and inherits no libwhisper/libggml search-path problems. Models are NOT baked:
# they are 142 MB (base.en) to 3 GB (large-v3), they change independently of the
# code, and they belong in the `models` volume. docker/entrypoint.sh fetches one
# on first boot.
# ---------------------------------------------------------------------------
FROM debian:bookworm-slim AS whisper-cpu
ARG WHISPER_REF
ARG WHISPER_BUILD_JOBS
ARG GGML_NATIVE
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
     build-essential cmake git ca-certificates \
  && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch "${WHISPER_REF}" \
      https://github.com/ggml-org/whisper.cpp /src/whisper.cpp
RUN cmake -S /src/whisper.cpp -B /src/whisper.cpp/build \
      -DCMAKE_BUILD_TYPE=Release \
      -DBUILD_SHARED_LIBS=OFF \
      -DGGML_NATIVE="${GGML_NATIVE}" \
      -DWHISPER_BUILD_TESTS=OFF \
      -DWHISPER_BUILD_SERVER=OFF \
  && cmake --build /src/whisper.cpp/build --config Release \
      -j "${WHISPER_BUILD_JOBS:-$(nproc)}" --target whisper-cli \
  && cp /src/whisper.cpp/build/bin/whisper-cli /usr/local/bin/whisper-cli \
  && strip /usr/local/bin/whisper-cli

# ---------------------------------------------------------------------------
# parakeet-vulkan — build parakeet-cli with the ggml Vulkan backend.
#
# On TRIXIE, and not by preference: ggml-vulkan uses vk::LayerSettingEXT, which
# arrived in the Vulkan headers around 1.3.272. Bookworm ships 1.3.239, so the
# build dies in ggml-vulkan.cpp with "'LayerSettingEXT' is not a member of 'vk'".
# Trixie has 1.4.309 and the matching glslc/glslangValidator. That is also why
# the Vulkan RUNTIME is trixie — this binary links trixie's libstdc++/glibc.
#
# Vulkan rather than a vendor SDK on purpose: one build runs on AMD (RADV),
# Intel and NVIDIA alike, and needs no proprietary runtime in the image — just
# the driver's ICD, which comes from mesa-vulkan-drivers in the runtime stage.
#
# The app already knows how to drive this and none of it is new app code:
# common/lib/transcriptionApps.ts registers a `parakeet` app whose binary is
# scripts/parakeet-stitch.mjs (the overlapping-window wrapper), which shells out
# to PARAKEET_CLI and passes the worker's device through as PARAKEET_DEVICE.
#
# Submodules are NOT optional — ggml lives in third_party/ggml, and a plain
# --depth 1 clone produces a tree that fails to configure.
# ---------------------------------------------------------------------------
FROM debian:trixie-slim AS parakeet-vulkan
ARG PARAKEET_REF
ARG WHISPER_BUILD_JOBS
ARG GGML_NATIVE
# The Vulkan backend needs more than the loader: ggml-vulkan compiles its
# compute shaders at BUILD time, so it wants glslc AND glslangValidator
# (glslang-tools), and it find_package()s SPIRV-Headers — without which cmake
# stops at "Could not find a package configuration file provided by
# SPIRV-Headers", several lines below a cheerful "Found Vulkan".
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
     build-essential cmake git ca-certificates \
     libvulkan-dev glslc glslang-tools spirv-headers spirv-tools \
  && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch "${PARAKEET_REF}" --recurse-submodules \
      https://github.com/mudler/parakeet.cpp /src/parakeet.cpp
# PARAKEET_GGML_VULKAN, *not* GGML_VULKAN — and the difference is silent.
# parakeet.cpp's CMakeLists does
#     set(GGML_VULKAN ${PARAKEET_GGML_VULKAN} CACHE BOOL "" FORCE)
# so passing GGML_VULKAN=ON is not ignored, it is OVERWRITTEN with OFF. The build
# then succeeds, prints only "Including CPU backend", ships no
# libggml-vulkan.so, and every transcription runs on the CPU with nothing
# anywhere saying why. Measured: that is exactly what the first build of this
# image did.
# Three steps, not one `&&` chain: chaining lets a cmake CONFIGURE error fall
# through to the guard's message, which then reports the wrong problem.
RUN set -eux; \
    cmake -S /src/parakeet.cpp -B /src/parakeet.cpp/build \
      -DCMAKE_BUILD_TYPE=Release \
      -DPARAKEET_GGML_VULKAN=ON \
      -DGGML_NATIVE="${GGML_NATIVE}" \
      -DPARAKEET_BUILD_TESTS=OFF \
      -DPARAKEET_BUILD_SERVER=OFF; \
    cmake --build /src/parakeet.cpp/build --config Release \
      -j "${WHISPER_BUILD_JOBS:-$(nproc)}"; \
    if [ -z "$(find /src/parakeet.cpp/build -name 'libggml-vulkan.so*' -print -quit)" ]; then \
      echo "FATAL: this build produced no Vulkan backend — see the comment above" >&2; \
      exit 1; \
    fi
# ggml builds each backend as its own shared object, nested a level deeper than
# the rest (third_party/ggml/src/ggml-vulkan/). A COPY glob does not recurse, so
# collect everything into one directory here — a missing libggml-vulkan.so is
# what silently turns a "GPU image" into a CPU one.
RUN set -eux; \
    mkdir -p /out/lib; \
    cp "$(find /src/parakeet.cpp/build -name parakeet-cli -type f -perm -u+x | head -1)" \
       /out/parakeet-cli; \
    find /src/parakeet.cpp/build -name '*.so*' -type f -exec cp -a {} /out/lib/ \;; \
    ls -1 /out/lib

# ---------------------------------------------------------------------------
# whisper-cuda — the same binary with CUDA offload, for the GPU overlay.
#
# Only ever built when something asks for the runtime-cuda target (BuildKit
# builds a target's graph, not the file), so an unreachable or wrong CUDA base
# never blocks the default `docker compose build`.
# ---------------------------------------------------------------------------
FROM ${CUDA_DEVEL_IMAGE} AS whisper-cuda
ARG WHISPER_REF
ARG WHISPER_BUILD_JOBS
ARG CUDA_ARCHITECTURES
ARG GGML_NATIVE
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
     build-essential cmake git ca-certificates \
  && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch "${WHISPER_REF}" \
      https://github.com/ggml-org/whisper.cpp /src/whisper.cpp
# Shared libs here: the CUDA backend links against the driver stack anyway, so a
# static build buys nothing, and the CUDA_ARCHITECTURES list keeps the image
# usable on more than the card it was built next to.
# The stubs directory is not optional. ggml's CUDA backend calls the DRIVER API
# (cuMemCreate, cuMemMap, … for its virtual-memory allocator), which lives in
# libcuda.so — shipped by the NVIDIA DRIVER, not by the toolkit, and therefore
# absent from a build container. The toolkit provides a link-time stub for
# exactly this case; without it the compile succeeds and the final link dies with
# a wall of "undefined reference to `cuMemCreate'". The real libcuda.so.1 is
# injected at run time by the NVIDIA container runtime.
#
# BOTH halves are needed: -L points at the stub directory, -lcuda actually links
# it. Adding only the -L changes nothing and fails identically — measured.
RUN cmake -S /src/whisper.cpp -B /src/whisper.cpp/build \
      -DCMAKE_BUILD_TYPE=Release \
      -DGGML_CUDA=ON \
      -DGGML_NATIVE="${GGML_NATIVE}" \
      -DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCHITECTURES}" \
      -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda" \
      -DCMAKE_SHARED_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda" \
      -DWHISPER_BUILD_TESTS=OFF \
      -DWHISPER_BUILD_SERVER=OFF \
  && cmake --build /src/whisper.cpp/build --config Release \
      -j "${WHISPER_BUILD_JOBS:-$(nproc)}" --target whisper-cli
# Collect the binary and EVERY shared object the build produced into one dir.
# A COPY glob does not recurse, and the CUDA backend's library is nested a level
# deeper than the rest (ggml/src/ggml-cuda/) — copying "the .so files in
# ggml/src" silently misses the one that makes this variant a GPU build at all.
RUN mkdir -p /out/lib \
  && cp /src/whisper.cpp/build/bin/whisper-cli /out/whisper-cli \
  && find /src/whisper.cpp/build -name '*.so*' -type f -exec cp -a {} /out/lib/ \;

# ---------------------------------------------------------------------------
# deps — install the workspace.
# ---------------------------------------------------------------------------
FROM ${NODE_IMAGE} AS deps

# pnpm as a plain global binary, NOT corepack — same reason Dockerfile.build
# gives: there is no `packageManager` pin in package.json, so corepack would try
# to fetch pnpm from the registry at run time.
RUN npm install -g pnpm@9.15.4

WORKDIR /repo

# Manifests first, for layer caching: this layer rebuilds only when a package.json
# or the lockfile moves. ALL SEVEN workspace packages listed in pnpm-workspace.yaml
# must be copied — a --frozen-lockfile install of a workspace with a missing member
# fails outright. (Dockerfile.build copies two because it only ever builds the
# export; that shortcut does not transfer here.)
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY common/package.json common/package.json
COPY editor/package.json editor/package.json
COPY export/package.json export/package.json
COPY homepage/package.json homepage/package.json
COPY mcp/package.json mcp/package.json
COPY umtool/report-to-video/package.json umtool/report-to-video/package.json
COPY umtool/package.json umtool/package.json

# `allowBuilds` / `onlyBuiltDependencies` in pnpm-workspace.yaml rebuild the
# native modules (lmdb, msgpackr-extract, esbuild) for THIS image's arch.
RUN pnpm install --frozen-lockfile

# ---------------------------------------------------------------------------
# build — compile the two server apps and the homepage.
# ---------------------------------------------------------------------------
FROM deps AS build

COPY . .

# There is no corpus at image-build time and there must not be: it is hundreds of
# GB, it is a separate git repo, and .dockerignore excludes it. Point the build
# at an empty one so anything that stats a corpus path finds an empty archive
# rather than a path that cannot exist.
ENV TRANSCRIPTS_DIR=/tmp/empty-corpus \
    NEXT_TELEMETRY_DISABLED=1
RUN mkdir -p /tmp/empty-corpus/channels /tmp/empty-corpus/sites

RUN pnpm --filter editor exec next build
RUN pnpm --filter umtool exec next build

# The homepage is the project's own marketing/docs site and is corpus-independent
# — build:nodata skips the compose+index data phase that needs one. The EXPORT
# site is deliberately NOT built here: it is a static render OF a corpus, so
# there is nothing to render until the operator has one. docker/publish-site.sh
# builds it at run time and publishes it into the shared volume the `site`
# service serves.
RUN pnpm --filter homepage run build:nodata

# ---------------------------------------------------------------------------
# runtime-base — everything the app needs EXCEPT a transcription backend.
#
# Shared by `runtime` and `runtime-vulkan` — the two differ only in RUNTIME_IMAGE
# (bookworm vs trixie) and which transcription backend they copy in.
# runtime-cuda cannot derive from it (it starts from an NVIDIA base image, not
# the node one) and so repeats it — that duplication is the price of a different
# base, not an oversight.
# ---------------------------------------------------------------------------
FROM ${RUNTIME_IMAGE} AS runtime-base

# ffmpeg/ffprobe: audio extraction and the download-time duration guard.
# zip/tar/xz/gzip: the export build's archive formats.
# rsync: the saved-video backup mirror.
# curl: model fetch + the compose healthcheck.
# git: the "cut release" flow commits, and yt-dlp resolves some extractors better
#      with it present.
# procps: `ps`, so "is the transcriber actually running in there?" is answerable
#      from a `docker compose exec` shell. debian:slim ships without it.
# aria2: archive.org files over BitTorrent (archive.org as the web seed), then
#      seeded for a while; without it they are downloaded directly.
# python3 + yt-dlp's optional modules (certifi, brotli, websockets, mutagen,
#      pycryptodome, requests): NOT for the yt-dlp below, which is a standalone
#      binary with its own python — for /usr/local/bin/yt-dlp-from-source, which
#      runs a yt-dlp SOURCE tree mounted at run time (docker-compose.ytdlp.yml).
# pipx (+ python3-venv, which it needs to make a venv): installs git-filter-repo
#      below, which the homepage's source mirror runs.
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
     ffmpeg aria2 zip unzip tar xz-utils gzip rsync curl ca-certificates git procps \
     python3 python3-venv pipx python3-certifi python3-brotli python3-websockets \
     python3-mutagen python3-pycryptodome python3-requests \
  && rm -rf /var/lib/apt/lists/*

# git-filter-repo, PINNED, for `archilyzer source publish` (the homepage's
# /source mirror). Installed, not left to `pipx run`, so a container publishes
# with no network fetch at build time. The version is the one
# common/publish/source.ts names in FILTER_REPO_PIPX_SPEC — a drift test
# (common/publish/buildImage.test.ts) holds every `pipx install` here to it.
# Into /opt + /usr/local/bin, not root's home: on PATH for every process.
RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install git-filter-repo==2.47.0 \
  && git filter-repo --version

# yt-dlp as the standalone release binary (it bundles its own python), installed
# writable so `yt-dlp -U` works — see YTDLP_AUTO_UPDATE in docker/entrypoint.sh.
# A pinned yt-dlp goes stale fast, and a stale yt-dlp is the single most common
# reason downloads start failing. It is THE IMAGE'S yt-dlp
# (ARCHILYZER_IMAGE_YTDLP below); YTDLP_BIN may name another one at run time —
# RUNNING_IN_DOCKER.md, "Substituting yt-dlp".
ARG TARGETARCH=amd64
RUN set -eux; \
    case "${TARGETARCH}" in \
      amd64) asset=yt-dlp_linux ;; \
      arm64) asset=yt-dlp_linux_aarch64 ;; \
      *) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
    esac; \
    curl -fsSL "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${asset}" \
      -o /usr/local/bin/yt-dlp; \
    chmod 0755 /usr/local/bin/yt-dlp; \
    /usr/local/bin/yt-dlp --version

# A JavaScript runtime for yt-dlp.
#
# Not optional any more, and easy to miss because it degrades rather than fails:
# without one, yt-dlp prints "No supported JavaScript runtime could be found ...
# some formats may be missing" and carries on with a reduced format list. deno is
# the one it enables by default. Measured inside this image before it was added —
# every YouTube extraction warned.
ARG DENO_VERSION=v2.9.5
RUN set -eux; \
    case "${TARGETARCH}" in \
      amd64) deno_asset=deno-x86_64-unknown-linux-gnu.zip ;; \
      arm64) deno_asset=deno-aarch64-unknown-linux-gnu.zip ;; \
      *) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
    esac; \
    curl -fsSL "https://github.com/denoland/deno/releases/download/${DENO_VERSION}/${deno_asset}" \
      -o /tmp/deno.zip; \
    unzip -q /tmp/deno.zip -d /usr/local/bin; \
    rm /tmp/deno.zip; \
    chmod 0755 /usr/local/bin/deno; \
    /usr/local/bin/deno --version

RUN npm install -g pnpm@9.15.4

# WORKDIR matters: findMonorepoRoot() (common/lib/paths.ts) walks UP from cwd
# looking for pnpm-workspace.yaml, and everything under it — the export staging
# dirs, the changelog paths, chart-templates.json — hangs off what it finds.
WORKDIR /repo
COPY --from=build /repo /repo

# The yt-dlp substitution hook: a wrapper that runs a yt-dlp SOURCE tree
# mounted at YTDLP_SOURCE_DIR (default /opt/yt-dlp-src) with the python above.
# Selected by YTDLP_BIN=/usr/local/bin/yt-dlp-from-source; see
# docker-compose.ytdlp.yml.
RUN ln -s /repo/docker/yt-dlp-from-source.sh /usr/local/bin/yt-dlp-from-source

# Data lives in volumes, never in the image. See docker-compose.yml.
# YTDLP_BIN is the one the app runs; ARCHILYZER_IMAGE_YTDLP is the one this
# image ships, so the entrypoint and `archilyzer doctor` can say "override".
ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    TRANSCRIPTS_DIR=/data/transcripts \
    SETTINGS_FILE=/data/config/settings.json \
    YTDLP_BIN=/usr/local/bin/yt-dlp \
    ARCHILYZER_IMAGE_YTDLP=/usr/local/bin/yt-dlp \
    EXPORT_INDEX_DIR=/data/builds/.export-index \
    EXPORT_BUILDS_DIR=/data/builds/.export-builds \
    ARCHILYZER_SITE_OUT=/data/builds/site
RUN mkdir -p /data/transcripts /data/config /data/models /data/builds

ENTRYPOINT ["/repo/docker/entrypoint.sh"]
CMD ["editor"]

# ---------------------------------------------------------------------------
# runtime — the published default target. CPU whisper.cpp.
# ---------------------------------------------------------------------------
FROM runtime-base AS runtime
COPY --from=whisper-cpu /usr/local/bin/whisper-cli /usr/local/bin/whisper-cli
ENV ARCHILYZER_TRANSCRIBER=whisper-cpp \
    WHISPER_BIN=/usr/local/bin/whisper-cli \
    WHISPER_MODEL=/data/models/ggml-base.en.bin

# Which commit and branch this image was built from — the publish stamps'
# `commit`/`branch` where there is no .git (.dockerignore keeps it out). Last,
# so a new commit re-runs one ENV layer and nothing else. Passed by compose from
# the shell: ARCHILYZER_COMMIT=$(git rev-parse HEAD) docker compose build.
ARG ARCHILYZER_COMMIT=
ARG ARCHILYZER_BRANCH=
ENV ARCHILYZER_COMMIT=${ARCHILYZER_COMMIT} \
    ARCHILYZER_BRANCH=${ARCHILYZER_BRANCH}

# ---------------------------------------------------------------------------
# runtime-vulkan — parakeet.cpp on any Vulkan GPU. docker-compose.vulkan.yml.
#
# Ships whisper-cli too: the GPU is for parakeet, but a CPU whisper worker is a
# useful thing to have in the same image — to compare against, or for when the
# GPU is busy.
# ---------------------------------------------------------------------------
FROM runtime-base AS runtime-vulkan

# libvulkan1 is the loader; mesa-vulkan-drivers supplies the ICDs that actually
# talk to the hardware (RADV for AMD, ANV for Intel). vulkan-tools is here so
# `vulkaninfo` can answer "does the container see the GPU at all?", which is the
# first question every time this does not work.
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
     libvulkan1 mesa-vulkan-drivers vulkan-tools \
  && rm -rf /var/lib/apt/lists/*

COPY --from=parakeet-vulkan /out/parakeet-cli /usr/local/bin/parakeet-cli
COPY --from=parakeet-vulkan /out/lib/ /usr/local/lib/
COPY --from=whisper-cpu /usr/local/bin/whisper-cli /usr/local/bin/whisper-cli
RUN ldconfig

ENV ARCHILYZER_TRANSCRIBER=parakeet \
    PARAKEET_CLI=/usr/local/bin/parakeet-cli \
    PARAKEET_MODEL=/data/models/tdt_ctc-110m-q8_0.gguf \
    WHISPER_BIN=/usr/local/bin/whisper-cli \
    WHISPER_MODEL=/data/models/ggml-base.en.bin

# Which commit and branch this image was built from — the publish stamps'
# `commit`/`branch` where there is no .git (.dockerignore keeps it out). Last,
# so a new commit re-runs one ENV layer and nothing else. Passed by compose from
# the shell: ARCHILYZER_COMMIT=$(git rev-parse HEAD) docker compose build.
ARG ARCHILYZER_COMMIT=
ARG ARCHILYZER_BRANCH=
ENV ARCHILYZER_COMMIT=${ARCHILYZER_COMMIT} \
    ARCHILYZER_BRANCH=${ARCHILYZER_BRANCH}

# ---------------------------------------------------------------------------
# runtime-cuda — whisper.cpp on CUDA. NVIDIA only. docker-compose.gpu.yml.
# ---------------------------------------------------------------------------
FROM ${CUDA_RUNTIME_IMAGE} AS runtime-cuda

ARG NODE_MAJOR=22
# The same python + pipx + git-filter-repo as runtime-base (read its comments).
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
     ffmpeg aria2 zip unzip tar xz-utils gzip rsync curl ca-certificates git gnupg procps \
     python3 python3-venv pipx python3-certifi python3-brotli python3-websockets \
     python3-mutagen python3-pycryptodome python3-requests \
  && curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
  && apt-get install -y --no-install-recommends nodejs \
  && rm -rf /var/lib/apt/lists/*
RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install git-filter-repo==2.47.0 \
  && git filter-repo --version

ARG TARGETARCH=amd64
RUN set -eux; \
    case "${TARGETARCH}" in \
      amd64) asset=yt-dlp_linux ;; \
      arm64) asset=yt-dlp_linux_aarch64 ;; \
      *) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
    esac; \
    curl -fsSL "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${asset}" \
      -o /usr/local/bin/yt-dlp; \
    chmod 0755 /usr/local/bin/yt-dlp; \
    /usr/local/bin/yt-dlp --version

# A JavaScript runtime for yt-dlp.
#
# Not optional any more, and easy to miss because it degrades rather than fails:
# without one, yt-dlp prints "No supported JavaScript runtime could be found ...
# some formats may be missing" and carries on with a reduced format list. deno is
# the one it enables by default. Measured inside this image before it was added —
# every YouTube extraction warned.
ARG DENO_VERSION=v2.9.5
RUN set -eux; \
    case "${TARGETARCH}" in \
      amd64) deno_asset=deno-x86_64-unknown-linux-gnu.zip ;; \
      arm64) deno_asset=deno-aarch64-unknown-linux-gnu.zip ;; \
      *) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
    esac; \
    curl -fsSL "https://github.com/denoland/deno/releases/download/${DENO_VERSION}/${deno_asset}" \
      -o /tmp/deno.zip; \
    unzip -q /tmp/deno.zip -d /usr/local/bin; \
    rm /tmp/deno.zip; \
    chmod 0755 /usr/local/bin/deno; \
    /usr/local/bin/deno --version

RUN npm install -g pnpm@9.15.4

# The CUDA whisper build is dynamically linked, so its ggml/whisper libraries
# come along with it.
COPY --from=whisper-cuda /out/whisper-cli /usr/local/bin/whisper-cli
COPY --from=whisper-cuda /out/lib/ /usr/local/lib/
RUN ldconfig

WORKDIR /repo
COPY --from=build /repo /repo
RUN ln -s /repo/docker/yt-dlp-from-source.sh /usr/local/bin/yt-dlp-from-source

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    TRANSCRIPTS_DIR=/data/transcripts \
    SETTINGS_FILE=/data/config/settings.json \
    ARCHILYZER_TRANSCRIBER=whisper-cpp \
    WHISPER_BIN=/usr/local/bin/whisper-cli \
    WHISPER_MODEL=/data/models/ggml-base.en.bin \
    YTDLP_BIN=/usr/local/bin/yt-dlp \
    ARCHILYZER_IMAGE_YTDLP=/usr/local/bin/yt-dlp \
    EXPORT_INDEX_DIR=/data/builds/.export-index \
    EXPORT_BUILDS_DIR=/data/builds/.export-builds \
    ARCHILYZER_SITE_OUT=/data/builds/site
RUN mkdir -p /data/transcripts /data/config /data/models /data/builds

ENTRYPOINT ["/repo/docker/entrypoint.sh"]
CMD ["editor"]

# Which commit and branch this image was built from — the publish stamps'
# `commit`/`branch` where there is no .git (.dockerignore keeps it out). Last,
# so a new commit re-runs one ENV layer and nothing else. Passed by compose from
# the shell: ARCHILYZER_COMMIT=$(git rev-parse HEAD) docker compose build.
ARG ARCHILYZER_COMMIT=
ARG ARCHILYZER_BRANCH=
ENV ARCHILYZER_COMMIT=${ARCHILYZER_COMMIT} \
    ARCHILYZER_BRANCH=${ARCHILYZER_BRANCH}
