model: команда memo model и автоскачивание модели (докачка, сверка размера, отключатель); manual: раздел про модель

This commit is contained in:
2026-10-02 04:53:04 +03:00
parent 69a0ebe0fa
commit 607137fdf7
5 changed files with 314 additions and 24 deletions
+80 -9
View File
@@ -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")