Проект: /root/WORK/memo (Kotlin/JVM). Заказ: скачивание модели — командой и автоматически. ## Зачем Сейчас модель (287 МБ) надо скачивать руками curl-ом по инструкции. Это единственный шаг, который ломает «взял и пользуешься». Нужно: (1) явная команда «просто скачай модель», (2) автоматическое скачивание, когда модель понадобилась, а её нет. ## Что уже известно про сервер (проверено) ```bash 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`). ```kotlin 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, val skipped: List, 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 } ``` Требования к `ensure`: 1. **Пропуск без сети.** Если файл существует, непустой и не `force` → он в `skipped`, **ни одного сетевого запроса** по нему не делается. Это важно: обычный запуск поиска офлайн обязан работать. 2. **Докачка.** Качать в `<имя>.part` рядом с целевым файлом. Если `.part` уже есть и непустой — продолжить с его размера, отправив `Range: bytes=-`; ответ `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` → скачивание не выполняется, а при отсутствии модели выдаётся внятная ошибка: `модель не найдена в ; запустите: memo model`. Сообщение о скачивании выводить в stderr с первой строкой вида `модель не найдена в , скачиваю с (≈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` → скачан заново. ## Обязательная сквозная проверка (приложить вывод) ```bash 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).