Files
memo/docs/orders/12-model-download.md
T

10 KiB
Raw Blame History

Проект: /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:

  1. Пропуск без сети. Если файл существует, непустой и не force → он в skipped, ни одного сетевого запроса по нему не делается. Это важно: обычный запуск поиска офлайн обязан работать.
  2. Докачка. Качать в <имя>.part рядом с целевым файлом. Если .part уже есть и непустой — продолжить с его размера, отправив Range: bytes=<size>-; ответ 206 → дописывать в конец; ответ 200 (сервер проигнорировал Range) → начать файл заново.
  3. Проверка размера. После завершения сравнить размер с Content-Length из того же ответа (или из HEAD). Не совпало → удалить .part, бросить IllegalStateException с обоими числами.
  4. Атомарность. Только после успешной проверки .part переименовывается в целевое имя (File.renameTo), чтобы оборванная закачка не оставила «валидный на вид» файл.
  5. Прогресс в stderr, не чаще раза в 2 секунды: скачиваю <имя>: 45% (128 МБ / 283 МБ). В stdout — ничего.
  6. Директорию создать (mkdirs), если её нет.
  7. Ошибки сети/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 со случайным портом. Никакого выхода в интернет.

  1. downloadsMissingFiles — на диске пусто, «сервер» отдаёт 2 файла → оба скачаны, размеры совпали, содержимое совпало побайтно, .part не остался.
  2. skipsExistingWithoutNetwork — файлы уже есть; сервер считать запросы (AtomicInteger) → после ensure счётчик равен 0, оба файла в skipped.
  3. resumesPartialDownload — в .part лежит первая половина файла; сервер на запрос с Range отвечает 206 с хвостом → итоговый файл полный, побайтно равен исходному.
  4. failsOnSizeMismatch — сервер объявляет Content-Length больше, чем реально отдаёт, и рвёт соединение → ensure бросает исключение, целевого файла нет, .part удалён.
  5. 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).