model: команда memo model и автоскачивание модели (докачка, сверка размера, отключатель); manual: раздел про модель
This commit is contained in:
@@ -68,21 +68,54 @@
|
||||
|
||||
```bash
|
||||
cd /root/WORK/memo
|
||||
|
||||
# 1. Собрать всё
|
||||
./gradlew installDist
|
||||
|
||||
# 2. Скачать модель (в артефакты не входит, 287 МБ, один раз)
|
||||
mkdir -p models/siglip2
|
||||
curl -fSL -o models/siglip2/text_model_int8.onnx http://static.binom.pw/models/siglip2/text_model_int8.onnx
|
||||
curl -fSL -o models/siglip2/tokenizer.model http://static.binom.pw/models/siglip2/tokenizer.model
|
||||
|
||||
# 3. Сказать, где модель (добавьте в ~/.bashrc, чтобы не повторять)
|
||||
export MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2
|
||||
```
|
||||
|
||||
Без переменной `MEMO_MODEL_DIR` программы ищут модель в `/root/WORK/memo/models/siglip2` —
|
||||
если у вас этот путь, шаг 3 можно пропустить.
|
||||
**Больше ничего делать не надо.** Модель (287 МБ) скачается сама при первом использовании —
|
||||
при первом `index`, `search` или `status`. Выглядит это так:
|
||||
|
||||
```
|
||||
модель не найдена в /root/WORK/memo/models/siglip2, скачиваю с http://static.binom.pw/models/siglip2 (≈287 МБ, один раз)
|
||||
скачиваю text_model_int8.onnx: 45% (128 МБ / 283 МБ)
|
||||
скачано text_model_int8.onnx: 283 МБ
|
||||
скачано tokenizer.model: 4 МБ
|
||||
```
|
||||
|
||||
Качается один раз: если файлы на месте, в сеть никто не ходит — поиск и индексация работают офлайн.
|
||||
|
||||
### Скачать заранее (необязательно)
|
||||
|
||||
Если хотите подготовить модель до первого запуска:
|
||||
|
||||
```bash
|
||||
CLI=/root/WORK/memo/memo-cli/build/install/memo/bin/memo
|
||||
$CLI model
|
||||
#> скачано: text_model_int8.onnx, tokenizer.model (287679278 байт)
|
||||
|
||||
$CLI model # повторный запуск: ничего не качает
|
||||
#> модель уже на месте в /root/WORK/memo/models/siglip2: text_model_int8.onnx, tokenizer.model
|
||||
|
||||
$CLI model --force # перекачать принудительно
|
||||
$CLI model --dir /другой/путь
|
||||
```
|
||||
|
||||
Скачивание можно прервать и продолжить: файл пишется как `<имя>.part`, а при следующем запуске
|
||||
закачка **дописывается с места обрыва** (сервер поддерживает докачку). Целевой файл появляется
|
||||
только после того, как размер сошёлся, — «битого, но выглядящего рабочим» файла не будет.
|
||||
|
||||
### Где живёт модель и как это менять
|
||||
|
||||
По умолчанию — `/root/WORK/memo/models/siglip2`. Одна копия на все три программы, поэтому
|
||||
в дистрибутивы она не кладётся (иначе было бы 287 МБ × 3).
|
||||
|
||||
| Переменная | Смысл |
|
||||
|---|---|
|
||||
| `MEMO_MODEL_DIR` | где лежит модель (иначе `/root/WORK/memo/models/siglip2`) |
|
||||
| `MEMO_MODEL_AUTO_DOWNLOAD=0` | запретить автоматическое скачивание. Тогда при отсутствии модели будет ошибка `модель не найдена в <путь>; запустите: memo model` |
|
||||
|
||||
```bash
|
||||
export MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2 # добавьте в ~/.bashrc, чтобы не повторять
|
||||
```
|
||||
|
||||
После сборки появятся три программы:
|
||||
|
||||
@@ -124,6 +157,7 @@ $CLI search ~/notes/infra "где публикуются релизы"
|
||||
|
||||
```bash
|
||||
$CLI index <путь> # проиндексировать (повторный прогон — быстрый)
|
||||
$CLI model # скачать модель (обычно не нужно — сама скачается)
|
||||
$CLI search <путь> "<вопрос>" # искать: и смысл, и точные слова
|
||||
$CLI search <путь> "<вопрос>" --k 5 # вернуть 5 результатов (по умолчанию 8)
|
||||
$CLI search <путь> "<вопрос>" --json # машинный вывод (для скриптов и агента)
|
||||
@@ -307,6 +341,11 @@ $CLI index ~/notes
|
||||
MCP-серверы читаются при старте Hermes: начните новую сессию. Проверьте, что путь к `memo-mcp`
|
||||
в конфиге верный и файл исполняемый.
|
||||
|
||||
**Агент долго не отвечает на первый запрос.**
|
||||
Скорее всего, скачивается модель (287 МБ) — в stderr `memo-mcp` идёт строка
|
||||
`модель не найдена ..., скачиваю ...`. Это разовое: скачайте заранее командой `$CLI model`
|
||||
или подложите файлы в `MEMO_MODEL_DIR`.
|
||||
|
||||
**Где посмотреть, что вообще происходит.**
|
||||
```bash
|
||||
$CLI status ~/notes # по каждой коллекции: файлов, чанков, когда индексировали
|
||||
|
||||
@@ -50,6 +50,7 @@ memo-mcp # MCP-сервер (stdio): memo_search / memo_status / memo_reinde
|
||||
./gradlew installDist # собрать все дистрибутивы
|
||||
|
||||
CLI=memo-cli/build/install/memo/bin/memo
|
||||
$CLI model # скачать модель (необязательно — скачается сама при первом запуске)
|
||||
$CLI index ~/notes # проиндексировать дерево (коллекции найдутся сами)
|
||||
$CLI search ~/notes "чем чинят карточку в jellyfin"
|
||||
$CLI search ~/notes "76.132" --mode lex --json
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
Проект: /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).
|
||||
@@ -4,6 +4,7 @@ import memo.core.Db
|
||||
import memo.core.Embedder
|
||||
import memo.core.Hit
|
||||
import memo.core.Indexer
|
||||
import memo.core.ModelStore
|
||||
import memo.core.RefreshHook
|
||||
import memo.core.SearchMode
|
||||
import memo.core.Searcher
|
||||
@@ -22,6 +23,7 @@ data class SearchCmd(
|
||||
) : Cmd
|
||||
data class StatusCmd(val path: String) : Cmd
|
||||
data class McpProbeCmd(val tool: String, val argsJson: String) : Cmd
|
||||
data class ModelCmd(val dir: String, val baseUrl: String, val force: Boolean) : Cmd
|
||||
data object HelpCmd : Cmd
|
||||
|
||||
fun parseArgs(args: Array<String>): Cmd {
|
||||
@@ -36,12 +38,40 @@ fun parseArgs(args: Array<String>): Cmd {
|
||||
val path = args.getOrNull(1) ?: return HelpCmd
|
||||
StatusCmd(path)
|
||||
}
|
||||
"model" -> parseModel(args.drop(1))
|
||||
"mcp-probe" -> parseMcpProbe(args.drop(1))
|
||||
"--help", "-h" -> HelpCmd
|
||||
else -> HelpCmd
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseModel(rest: List<String>): Cmd {
|
||||
var dir = System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2"
|
||||
var url = ModelStore.DEFAULT_BASE_URL
|
||||
var force = false
|
||||
var i = 0
|
||||
while (i < rest.size) {
|
||||
when (rest[i]) {
|
||||
"--dir" -> {
|
||||
val v = rest.getOrNull(i + 1) ?: return HelpCmd
|
||||
dir = v
|
||||
i += 2
|
||||
}
|
||||
"--url" -> {
|
||||
val v = rest.getOrNull(i + 1) ?: return HelpCmd
|
||||
url = v
|
||||
i += 2
|
||||
}
|
||||
"--force" -> {
|
||||
force = true
|
||||
i += 1
|
||||
}
|
||||
else -> return HelpCmd
|
||||
}
|
||||
}
|
||||
return ModelCmd(dir, url, force)
|
||||
}
|
||||
|
||||
private fun parseSearch(rest: List<String>): Cmd {
|
||||
if (rest.size < 2) return HelpCmd
|
||||
val path = rest[0]
|
||||
@@ -100,12 +130,18 @@ private fun parseMcpProbe(rest: List<String>): Cmd {
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
when (val cmd = parseArgs(args)) {
|
||||
is IndexCmd -> runIndex(cmd)
|
||||
is SearchCmd -> runSearch(cmd)
|
||||
is StatusCmd -> runStatus(cmd)
|
||||
is McpProbeCmd -> runMcpProbe(cmd)
|
||||
HelpCmd -> printHelp()
|
||||
try {
|
||||
when (val cmd = parseArgs(args)) {
|
||||
is IndexCmd -> runIndex(cmd)
|
||||
is SearchCmd -> runSearch(cmd)
|
||||
is StatusCmd -> runStatus(cmd)
|
||||
is ModelCmd -> runModel(cmd)
|
||||
is McpProbeCmd -> runMcpProbe(cmd)
|
||||
HelpCmd -> printHelp()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
System.err.println(t.message ?: t.toString())
|
||||
kotlin.system.exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +152,46 @@ private fun printHelp() {
|
||||
memo index <path>
|
||||
memo search <path> <query> [--k N] [--mode hybrid|lex|vec] [--json]
|
||||
memo status <path>
|
||||
memo model [--dir <path>] [--url <base>] [--force]
|
||||
memo mcp-probe --tool <name> [--args <json>]
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun modelPaths(): Pair<String, String> {
|
||||
val modelDir = System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2"
|
||||
return "$modelDir/text_model_int8.onnx" to "$modelDir/tokenizer.model"
|
||||
private fun modelDir(): File =
|
||||
File(System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2")
|
||||
|
||||
private fun modelPaths(): Pair<String, String> = ModelStore.paths(modelDir())
|
||||
|
||||
private fun ensureModel(dir: File) {
|
||||
if (ModelStore.isComplete(dir)) return
|
||||
if (System.getenv("MEMO_MODEL_AUTO_DOWNLOAD") == "0") {
|
||||
val msg = "модель не найдена в ${dir.absolutePath}; запустите: memo model"
|
||||
System.err.println(msg)
|
||||
throw IllegalStateException(msg)
|
||||
}
|
||||
System.err.println(
|
||||
"модель не найдена в ${dir.absolutePath}, скачиваю с ${ModelStore.DEFAULT_BASE_URL} (≈287 МБ, один раз)",
|
||||
)
|
||||
ModelStore.ensure(dir, log = { System.err.println(it) })
|
||||
}
|
||||
|
||||
private fun runModel(cmd: ModelCmd) {
|
||||
val dir = File(cmd.dir)
|
||||
val result = ModelStore.ensure(
|
||||
dir = dir,
|
||||
baseUrl = cmd.baseUrl,
|
||||
force = cmd.force,
|
||||
log = { System.err.println(it) },
|
||||
)
|
||||
if (result.downloaded.isEmpty()) {
|
||||
println("модель уже на месте в ${dir.absolutePath}: ${result.skipped.joinToString(", ")}")
|
||||
} else {
|
||||
println("скачано: ${result.downloaded.joinToString(", ")} (${result.bytes} байт)")
|
||||
if (result.skipped.isNotEmpty()) {
|
||||
println("уже было: ${result.skipped.joinToString(", ")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runIndex(cmd: IndexCmd) {
|
||||
@@ -134,6 +202,7 @@ private fun runIndex(cmd: IndexCmd) {
|
||||
return
|
||||
}
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
ensureModel(modelDir())
|
||||
var totalUpdated = 0
|
||||
for (coll in collections) {
|
||||
val memoDir = File(coll, ".memo")
|
||||
@@ -168,6 +237,7 @@ private fun runSearch(cmd: SearchCmd) {
|
||||
return
|
||||
}
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
ensureModel(modelDir())
|
||||
val allHits = ArrayList<Hit>()
|
||||
for (coll in targets) {
|
||||
val dbPath = File(coll, ".memo/index.db")
|
||||
@@ -325,6 +395,7 @@ private fun performSearch(path: String, query: String, k: Int, mode: SearchMode)
|
||||
val targets = resolveSearchTargets(File(path))
|
||||
if (targets.isEmpty()) return emptyList()
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
ensureModel(modelDir())
|
||||
val allHits = ArrayList<Hit>()
|
||||
for (coll in targets) {
|
||||
val dbPath = File(coll, ".memo/index.db")
|
||||
|
||||
@@ -3,6 +3,7 @@ package memo.mcp
|
||||
import memo.core.Db
|
||||
import memo.core.Embedder
|
||||
import memo.core.Indexer
|
||||
import memo.core.ModelStore
|
||||
import memo.core.RefreshHook
|
||||
import memo.core.SearchMode
|
||||
import memo.core.Searcher
|
||||
@@ -115,6 +116,7 @@ fun toolSearch(path: String, query: String, k: Int, mode: SearchMode): String {
|
||||
val root = resolveCollection(File(path))
|
||||
val targets = selectTargets(root)
|
||||
if (targets.isEmpty()) return "коллекции не найдены"
|
||||
ensureModel()
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
val allHits = ArrayList<memo.core.Hit>()
|
||||
val errors = ArrayList<String>()
|
||||
@@ -200,6 +202,7 @@ fun toolReindex(path: String): String {
|
||||
val root = resolveCollection(File(path))
|
||||
val collections = selectTargets(root)
|
||||
if (collections.isEmpty()) return "коллекции не найдены"
|
||||
ensureModel()
|
||||
val (modelPath, tokenizerPath) = modelPaths()
|
||||
var totalUpdated = 0
|
||||
val embedder = Embedder(modelPath, tokenizerPath)
|
||||
@@ -231,9 +234,23 @@ private fun selectTargets(root: File): List<File> {
|
||||
return findCollections(root)
|
||||
}
|
||||
|
||||
private fun modelPaths(): Pair<String, String> {
|
||||
val modelDir = System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2"
|
||||
return "$modelDir/text_model_int8.onnx" to "$modelDir/tokenizer.model"
|
||||
private fun modelDir(): File =
|
||||
File(System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2")
|
||||
|
||||
private fun modelPaths(): Pair<String, String> = ModelStore.paths(modelDir())
|
||||
|
||||
private fun ensureModel() {
|
||||
val dir = modelDir()
|
||||
if (ModelStore.isComplete(dir)) return
|
||||
if (System.getenv("MEMO_MODEL_AUTO_DOWNLOAD") == "0") {
|
||||
val msg = "модель не найдена в ${dir.absolutePath}; запустите: memo model"
|
||||
System.err.println(msg)
|
||||
throw IllegalStateException(msg)
|
||||
}
|
||||
System.err.println(
|
||||
"модель не найдена в ${dir.absolutePath}, скачиваю с ${ModelStore.DEFAULT_BASE_URL} (≈287 МБ, один раз)",
|
||||
)
|
||||
ModelStore.ensure(dir, log = { System.err.println(it) })
|
||||
}
|
||||
|
||||
private fun countInt(db: Db, sql: String): Int {
|
||||
|
||||
Reference in New Issue
Block a user