Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

99
deploy/llm/download-model.sh Executable file
View File

@@ -0,0 +1,99 @@
#!/bin/sh
# Идемпотентное скачивание GGUF-модели для локального LLM-сервера
# (llama.cpp), см. ADR-004 (`docs/architecture/adr/004-ai-tier-matrix.md`).
#
# Генерализовано под 3 уровня AI (min/medium/max, разные модели семейства
# Qwen3.5) — конкретную модель задают переменные окружения, которые пишет
# `install.sh` при выборе пресета 3/4/5 (см. `docs/deploy/llm-setup.md`):
# LLM_MODEL_FILE — имя файла модели внутри $MODEL_DIR (ПОД ЭТИМ именем
# сохраняется локально — не обязано совпадать с
# именем файла в источнике, см. GGUF_URL/GGUF_FILE_NAME)
# LLM_MODEL_URL — прямая ссылка на GGUF (resolve/main/...)
# LLM_MODEL_MIN_SIZE — ожидаемый минимальный размер файла в байтах
# (страховка от оборванной/битой докачки)
# LLM_TOKENIZER_FILE — имя файла токенизатора внутри $MODEL_DIR
# LLM_TOKENIZER_URL — ссылка на tokenizer.json базовой модели (нужен
# QwenTokenCounter, backend/core/summarization/tokens.py)
#
# Имена файлов (LLM_MODEL_FILE/LLM_TOKENIZER_FILE) — КОНТРАКТ с
# `backend/services/ai_tiers.py` (см. докстринг модуля): детект доступности
# уровня AI и сам плагин `qwen_local` (`tokenizer_path`/`model` в
# `config/plugins.yaml`) ищут файлы по путям `{QWEN_MODELS_ROOT}/{model}.gguf`
# и `{QWEN_MODELS_ROOT}/{model}.tokenizer.json` из этого модуля — install.sh
# обязан передавать сюда ИМЕННО эти имена, не имена файлов в источнике.
#
# Дефолты ниже соответствуют уровню `min` (Qwen3.5-4B-Instruct, ADR-004) —
# скрипт можно запускать вручную без install.sh (см. `docs/deploy/llm-setup.md`).
#
# Запускается init-контейнером `llm-model-init` (образ curlimages/curl,
# профили compose `llm`/`llm-gpu`) перед стартом сервиса `llm`/`llm-gpu`.
#
# Идемпотентность: перед скачиванием каждого файла проверяется его
# наличие и размер (не меньше ожидаемого минимума) — повторный запуск
# на уже заполненном томе (в т.ч. после смены пресета на модель ДРУГОГО
# уровня, ранее уже скачанную в этот же volume) ничего не перекачивает.
set -eu
MODEL_DIR="${MODEL_DIR:-/models/qwen}"
GGUF_URL="${LLM_MODEL_URL:-https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf}"
GGUF_FILE_NAME="${LLM_MODEL_FILE:-qwen3.5-4b-instruct-q4_k_m.gguf}"
GGUF_FILE="${MODEL_DIR}/${GGUF_FILE_NAME}"
# Порог занижен относительно реального размера файла — не ловить ложные
# срабатывания на разных ревизиях модели, но отсекать битые/оборванные
# докачки (пустой или сильно урезанный файл). install.sh передаёт более
# точный порог для выбранного уровня.
GGUF_MIN_SIZE="${LLM_MODEL_MIN_SIZE:-2000000000}"
TOKENIZER_URL="${LLM_TOKENIZER_URL:-https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json}"
TOKENIZER_FILE_NAME="${LLM_TOKENIZER_FILE:-qwen3.5-4b-instruct.tokenizer.json}"
TOKENIZER_FILE="${MODEL_DIR}/${TOKENIZER_FILE_NAME}"
# tokenizer.json семейства Qwen3.5 занимает ~12 МБ (словарь + merges) —
# порог просто отсекает пустой/HTML-ответ вместо файла.
TOKENIZER_MIN_SIZE=$((1 * 1000 * 1000))
# Размер существующего файла в байтах (0, если файла нет).
file_size() {
if [ -f "$1" ]; then
wc -c < "$1" | tr -d ' '
else
echo 0
fi
}
# Скачать файл по URL, если он отсутствует или меньше минимального размера.
download_if_missing() {
url="$1"
dest="$2"
min_size="$3"
name="$4"
size=$(file_size "$dest")
if [ "$size" -ge "$min_size" ]; then
echo "[download-model] ${name}: уже скачан (${dest}, ${size} байт) — пропуск"
return 0
fi
echo "[download-model] ${name}: скачивание из ${url} в ${dest}"
tmp="${dest}.part"
# -f — считать HTTP-ошибки (4xx/5xx) провалом; -L — следовать
# редиректам HuggingFace на CDN; --retry — устойчивость к обрывам сети.
curl -fL --retry 5 --retry-delay 5 -o "$tmp" "$url"
new_size=$(file_size "$tmp")
if [ "$new_size" -lt "$min_size" ]; then
echo "[download-model] ${name}: скачанный файл подозрительно мал (${new_size} байт) — прерывание" >&2
rm -f "$tmp"
exit 1
fi
mv "$tmp" "$dest"
echo "[download-model] ${name}: готово (${dest}, ${new_size} байт)"
}
mkdir -p "$MODEL_DIR"
download_if_missing "$GGUF_URL" "$GGUF_FILE" "$GGUF_MIN_SIZE" "GGUF-модель (${GGUF_FILE_NAME})"
download_if_missing "$TOKENIZER_URL" "$TOKENIZER_FILE" "$TOKENIZER_MIN_SIZE" "${TOKENIZER_FILE_NAME}"
echo "[download-model] всё готово: ${MODEL_DIR}"