whisper-server: validate model downloads (size + ggml magic), self-heal poisoned files

The first server deployment downloaded only 1101 bytes (a redirect/error
page) and the entrypoint promoted it to ggml-large-v3.bin, after which
every restart skipped the download and the server ran without a valid
model. Now both fresh downloads and existing files are validated
(minimum 50 MB + the ggml magic bytes); invalid files are logged,
deleted, and re-downloaded, and a failed download dumps the first 400
bytes of what was actually received for diagnosis before exiting.

Validated locally: a poisoned model file is detected, removed, and a
valid one re-downloaded; inference served correctly afterwards.
This commit is contained in:
2026-09-07 12:37:38 +02:00
parent ce1880dc90
commit e97e8b1d33
+39 -5
View File
@@ -2,6 +2,11 @@
# Downloads the configured GGML model on first start, then runs whisper-server. # Downloads the configured GGML model on first start, then runs whisper-server.
# Uses the GPU (Vulkan) automatically when one is present; set NO_GPU=1 # Uses the GPU (Vulkan) automatically when one is present; set NO_GPU=1
# to force CPU. # to force CPU.
#
# Both the freshly downloaded file and any existing file are validated
# (minimum size + ggml magic bytes) so a truncated download or a saved
# error/redirect page can never be mistaken for a model — a poisoned
# file is deleted and re-downloaded instead.
set -eu set -eu
MODEL="${MODEL:-small}" MODEL="${MODEL:-small}"
@@ -10,14 +15,43 @@ HOST="${HOST:-0.0.0.0}"
PORT="${PORT:-8085}" PORT="${PORT:-8085}"
MODEL_DIR="${MODEL_DIR:-/models}" MODEL_DIR="${MODEL_DIR:-/models}"
FILE="$MODEL_DIR/ggml-$MODEL.bin" FILE="$MODEL_DIR/ggml-$MODEL.bin"
URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-$MODEL.bin"
# smallest supported model (ggml-tiny.bin) is ~75 MB
MIN_SIZE=50000000
GGML_MAGIC="$(printf 'lmgg')" # GGML_FILE_MAGIC 0x67676d6c, little-endian
is_valid_model() {
[ -s "$1" ] || return 1
[ "$(stat -c%s "$1")" -ge "$MIN_SIZE" ] || return 1
[ "$(head -c 4 "$1")" = "$GGML_MAGIC" ] || return 1
return 0
}
mkdir -p "$MODEL_DIR" mkdir -p "$MODEL_DIR"
if [ ! -s "$FILE" ]; then
echo "downloading ggml-$MODEL.bin to $FILE ..." if ! is_valid_model "$FILE" && [ -e "$FILE" ]; then
curl -fL --retry 3 -C - -o "$FILE.part" \ echo "existing $FILE is invalid ($(stat -c%s "$FILE") bytes) — " \
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-$MODEL.bin" "deleting and re-downloading"
fi
if ! is_valid_model "$FILE"; then
rm -f "$FILE" "$FILE.part"
echo "downloading $URL to $FILE ..."
curl -fLS --retry 3 --retry-delay 5 -C - -o "$FILE.part" "$URL"
if ! is_valid_model "$FILE.part"; then
size="$(stat -c%s "$FILE.part" 2>/dev/null || echo 0)"
echo "ERROR: download is not a valid model ($size bytes)."
echo "------- first 400 bytes of the response (probably an error/redirect page):"
head -c 400 "$FILE.part"
echo ""
echo "------- end of response"
rm -f "$FILE.part"
exit 1
fi
mv "$FILE.part" "$FILE" mv "$FILE.part" "$FILE"
echo "done." echo "downloaded $(stat -c%s "$FILE") bytes, magic ok."
fi fi
GPU_FLAGS="" GPU_FLAGS=""