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

163 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Проект: /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<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` → скачан заново.
## Обязательная сквозная проверка (приложить вывод)
```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).