10 KiB
Проект: /root/WORK/memo (Kotlin/JVM). Заказ: скачивание модели — командой и автоматически.
Зачем
Сейчас модель (287 МБ) надо скачивать руками curl-ом по инструкции. Это единственный шаг, который ломает «взял и пользуешься». Нужно: (1) явная команда «просто скачай модель», (2) автоматическое скачивание, когда модель понадобилась, а её нет.
Что уже известно про сервер (проверено)
curl -sSIL http://static.binom.pw/models/siglip2/text_model_int8.onnx
# HTTP/1.1 200 OK, Content-Length: 283438275, Etag: "6a9d4b7c-10e4ecc3"
curl -sSIL http://static.binom.pw/models/siglip2/tokenizer.model
# HTTP/1.1 200 OK, Content-Length: 4241003
curl -sS -r 100-199 -D - http://static.binom.pw/models/siglip2/tokenizer.model
# HTTP/1.1 206 Partial Content, Content-Range: bytes 100-199/4241003, Accept-Ranges: bytes
Сервер поддерживает докачку (Range) и отдаёт Content-Length. Файлов контрольных сумм
на сервере НЕТ (.sha256 → 404), листинг каталога закрыт (403). Значит, проверка целостности —
по размеру из Content-Length, а не по хэшу.
Что сделать
1. memo-core: ModelStore.kt (новый файл, пакет memo.core)
Объект/класс без внешних зависимостей — только JDK 21 (java.net.http.HttpClient).
object ModelStore {
const val DEFAULT_BASE_URL = "http://static.binom.pw/models/siglip2"
val FILES = listOf("text_model_int8.onnx", "tokenizer.model")
data class Result(val downloaded: List<String>, val skipped: List<String>, val bytes: Long)
/** Гарантирует наличие всех файлов модели в dir. Возвращает имена скачанных/пропущенных. */
fun ensure(
dir: java.io.File,
baseUrl: String = DEFAULT_BASE_URL,
force: Boolean = false,
log: (String) -> Unit = {},
timeoutMillis: Long = 60_000,
): Result
/** Пути к модели в том же порядке (modelPath, tokenizerPath). */
fun paths(dir: java.io.File): Pair<String, String>
}
Требования к ensure:
- Пропуск без сети. Если файл существует, непустой и не
force→ он вskipped, ни одного сетевого запроса по нему не делается. Это важно: обычный запуск поиска офлайн обязан работать. - Докачка. Качать в
<имя>.partрядом с целевым файлом. Если.partуже есть и непустой — продолжить с его размера, отправивRange: bytes=<size>-; ответ206→ дописывать в конец; ответ200(сервер проигнорировал Range) → начать файл заново. - Проверка размера. После завершения сравнить размер с
Content-Lengthиз того же ответа (или изHEAD). Не совпало → удалить.part, броситьIllegalStateExceptionс обоими числами. - Атомарность. Только после успешной проверки
.partпереименовывается в целевое имя (File.renameTo), чтобы оборванная закачка не оставила «валидный на вид» файл. - Прогресс в stderr, не чаще раза в 2 секунды:
скачиваю <имя>: 45% (128 МБ / 283 МБ). В stdout — ничего. - Директорию создать (
mkdirs), если её нет. - Ошибки сети/HTTP-кода (не 200/206) — исключение с понятным текстом и именем файла.
2. memo-cli: команда memo model
memo model [--dir <путь>] [--url <база>] [--force]
- без
--dir— директория изMEMO_MODEL_DIR(или/root/WORK/memo/models/siglip2, как уже заведено вmodelPaths()); - печатает по-русски, что скачано, что уже было, сколько байт;
- exit 0 при успехе, 1 при ошибке.
- Добавить эту команду в текст
usage(он печатается вHelpCmd).
3. Автоматическое скачивание
Во всех точках, где модель нужна (memo-cli index/search/status, memo-mcp при старте):
перед созданием Embedder вызвать ModelStore.ensure(dir) — если файлов нет или они пустые.
Если файлы на месте — вызова сети не происходит (см. п.1), поведение не меняется.
Отключение: переменная MEMO_MODEL_AUTO_DOWNLOAD=0 → скачивание не выполняется, а при отсутствии
модели выдаётся внятная ошибка: модель не найдена в <dir>; запустите: memo model.
Сообщение о скачивании выводить в stderr с первой строкой вида
модель не найдена в <dir>, скачиваю с <url> (≈287 МБ, один раз).
4. Тесты: memo-core/src/test/kotlin/memo/core/ModelStoreTest.kt (новый файл)
Поднять локальный HTTP-сервер на JDK (com.sun.net.httpserver.HttpServer, без зависимостей),
слушать на 127.0.0.1 со случайным портом. Никакого выхода в интернет.
downloadsMissingFiles— на диске пусто, «сервер» отдаёт 2 файла → оба скачаны, размеры совпали, содержимое совпало побайтно,.partне остался.skipsExistingWithoutNetwork— файлы уже есть; сервер считать запросы (AtomicInteger) → послеensureсчётчик равен 0, оба файла вskipped.resumesPartialDownload— в.partлежит первая половина файла; сервер на запрос сRangeотвечает206с хвостом → итоговый файл полный, побайтно равен исходному.failsOnSizeMismatch— сервер объявляетContent-Lengthбольше, чем реально отдаёт, и рвёт соединение →ensureбросает исключение, целевого файла нет,.partудалён.forceRedownloads— файл есть,force = true→ скачан заново.
Обязательная сквозная проверка (приложить вывод)
cd /root/WORK/memo
./gradlew installDist -q
CLI=memo-cli/build/install/memo/bin/memo
# 1) пустая директория: команда model скачивает (база — локальный сервер, НЕ интернет, чтобы
# проверка была воспроизводимой; для этого в проверке используем свой http-сервер на python)
rm -rf /tmp/memo-models && mkdir -p /tmp/memo-models/src
cp models/siglip2/* /tmp/memo-models/src/
cd /tmp/memo-models/src && python3 -m http.server 18999 > /tmp/memo-http.log 2>&1 &
HPID=$!
sleep 2
cd /root/WORK/memo
MEMO_MODEL_DIR=/tmp/memo-models/dst $CLI model --dir /tmp/memo-models/dst --url http://127.0.0.1:18999
echo "--- что появилось:"; ls -l /tmp/memo-models/dst; md5sum /tmp/memo-models/dst/* models/siglip2/*
# 2) повторный запуск: ничего не качает
MEMO_MODEL_DIR=/tmp/memo-models/dst $CLI model --dir /tmp/memo-models/dst --url http://127.0.0.1:18999
# 3) автоскачивание: поиск на пустом месте сам тянет модель
rm -rf /tmp/memo-models/dst2
MEMO_MODEL_DIR=/tmp/memo-models/dst2 $CLI index /root/WORK/memo-e2e 2>&1 | head -4
ls -l /tmp/memo-models/dst2
# 4) отключение автоскачивания
rm -rf /tmp/memo-models/dst3
MEMO_MODEL_AUTO_DOWNLOAD=0 MEMO_MODEL_DIR=/tmp/memo-models/dst3 $CLI index /root/WORK/memo-e2e; echo "exit=$?"
kill $HPID 2>/dev/null
Ожидается: (1) оба файла скачаны, md5 совпадают с оригиналом, .part нет; (2) во второй раз
«уже на месте», сеть не тронута; (3) при автоскачивании модель появилась и индексация прошла;
(4) с MEMO_MODEL_AUTO_DOWNLOAD=0 — ошибка с подсказкой memo model, ненулевой код.
Важно: шаг 3 и 4 проверяют автоскачивание с продакшн-URL по умолчанию (интернет доступен в этом окружении). Если интернета в момент проверки нет — приложить вывод и явно сказать об этом.
После: ./gradlew test --rerun-tasks — все тесты зелёные (было 36, станет больше).
Коммит осмысленным сообщением.
Обновить документацию
MANUAL.md: в §3 убрать ручной curl и написать, что модель скачивается сама при первом использовании, плюс команда$CLI modelдля скачивания заранее; упомянутьMEMO_MODEL_AUTO_DOWNLOAD=0.README.md: в разделе «Быстрый старт» — строка проmemo model.
СТРОГИЕ ЗАПРЕТЫ
- Ни одной новой внешней зависимости (только JDK).
- Не менять
memo-core's схему БД,Chunker,Searcher,Indexer,Collections. - Не менять смысл существующих тестов.
- Не выводить план текстом; сразу правь файлы.
- В тестах не ходить в интернет (только локальный HttpServer).