build_models.py
Rama main · réplica de lectura del repositorio original.
"""
Curatarr - Build Ollama Models
Pulls base models if not already present, then bakes system prompts
into curatarr-curator and curatarr-summarizer.
Run once after changing BASE_CURATOR_MODEL or BASE_SUMMARIZER_MODEL:
python build_models.py
A split install (Curatarr on a NAS, the gaming PC's card as a second host:
OLLAMA_PC_ENDPOINT) builds the same bake names on each machine, from a base
that fits its card:
python build_models.py --host main this install's Ollama (BASE_*)
python build_models.py --host pc the PC's (PC_BASE_*, its window)
python build_models.py --suggest 16 what the bench recommends for a 16 GB card
python build_models.py --check both hosts, and the embedding weights
"""
import argparse
import asyncio
import sys
sys.path.insert(0, ".")
# Windows consoles default to cp1252, which crashes on the → / ✅ in our
# prints. Force UTF-8 so the rebuild never dies on a status line.
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
from src.services.setup_wizard import build_ollama_models
from src.config import settings
async def main(host: str = "main"):
from src.services.model_check import host_plan
plan = host_plan(host)
endpoint = plan["url"]
curator_base = plan["curator"]
summarizer_base = plan["summarizer"]
# Two-bake split: only build the pitcher when the split is enabled
# (PITCHER_MODEL set in .env). Gating on BASE_PITCHER_MODEL instead
# would pull 17 GB on every fresh install. On a split install it lives
# on the PC only: deletion runs wait for the PC.
pitcher_base = plan["pitcher"]
print(f"\nHost : {host} ({endpoint}, curator window {plan['num_ctx']})")
print(f"Curator base : {curator_base} → curatarr-curator")
print(f"Summarizer base : {summarizer_base} → curatarr-summarizer")
if pitcher_base:
print(f"Pitcher base : {pitcher_base} → curatarr-pitcher")
else:
print("Pitcher : not on this host — skipping")
if not plan["embedding"]:
print("Embeddings : not on this host (a split install embeds on the main host)")
print("\nMissing models will be pulled automatically.\n")
print("─" * 60)
results = await build_ollama_models(endpoint, curator_base, summarizer_base,
base_pitcher=pitcher_base, num_ctx=plan["num_ctx"],
with_embedding=bool(plan["embedding"]))
print("─" * 60)
if results.get("curator"):
print("✅ curatarr-curator ready")
else:
print(f"❌ curatarr-curator failed")
print(f" Is '{curator_base}' available on Ollama Hub?")
if results.get("summarizer"):
print("✅ curatarr-summarizer ready")
else:
print(f"❌ curatarr-summarizer failed")
print(f" Is '{summarizer_base}' available on Ollama Hub?")
if pitcher_base:
if results.get("pitcher"):
print("✅ curatarr-pitcher ready")
else:
print(f"❌ curatarr-pitcher failed")
print(f" Is '{pitcher_base}' available on Ollama Hub?")
if plan["embedding"]:
if results.get("embedding"):
print(f"✅ {plan['embedding']} (embeddings) ready")
else:
print(f"❌ {plan['embedding']} (embeddings) failed")
print(f" Could not pull '{plan['embedding']}' — without it, enrichment")
print(f" produces text but no vectors (every item stays vector_ready=0).")
if all(results.values()):
print("\n🎬 All models ready — restart Curatarr.\n")
else:
print("\n⚠️ Fix the errors above and run this script again.\n")
sys.exit(1)
def check() -> int:
"""``--check``: 0 = every model each host runs on is present, 1 = some
are missing (named; start.bat builds them), 2 = an Ollama does not
answer, 3 = the main host's embedding weights differ from the ones the
stored vectors were made with (a build cannot fix that - start.bat only
warns). Over HTTP, so it works in the container too. The first check
records which embedding weights the vectors come from - run it on the
old machine before a move."""
from src.services.model_check import (curator_fit_warning, host_plan, missing_on,
record_embedding_digest)
from src.services.ollama_hosts import split
rc = 0
for host in (["main", "pc"] if split() else ["main"]):
plan = host_plan(host)
missing = missing_on(plan["url"], plan["models"])
if missing is None:
print(f"[{host}] Ollama at {plan['url']} is not answering - its models cannot be checked.")
rc = max(rc, 2)
elif missing:
print(f"[{host}] Missing Ollama models: " + ", ".join(missing)
+ f" (python build_models.py --host {host})")
rc = max(rc, 1)
else:
print(f"[{host}] Ollama models present: " + ", ".join(plan["models"]))
ident = record_embedding_digest()
if ident["status"] == "mismatch":
print(f"Embedding weights DIFFER: {ident['model']} on the main host is "
f"{(ident['host'] or '')[:12]}, the stored vectors were made with "
f"{ident['recorded'][:12]}. New vectors would not compare with the old ones - "
f"pull the same weights, or re-embed the library.")
rc = 3 if rc == 0 else rc
elif ident["status"] == "match":
print(f"Embedding weights match the stored vectors ({ident['model']} "
f"{ident['recorded'][:12]}).")
warning = curator_fit_warning()
if warning:
# A warning, not a failure: the models ARE present, and an operator
# who set CURATOR_NUM_GPU=-1 on purpose has chosen the slow path.
print("Warning: " + warning)
return rc
def suggest(vram_gb: float) -> int:
"""What the hardware bench recommends for a card of ``vram_gb``. Reads
the bench's matrix only - talks to no Ollama."""
from src.services.model_catalog import hardware_package
pkg = hardware_package(vram_gb)
rec = (pkg or {}).get("recommended")
if not rec:
print((pkg or {}).get("note") or "No bench data for this card.")
return 1
exp = rec.get("expected") or {}
print(f"{vram_gb:g} GB card - the bench's package (tier {pkg['tier_gb']} GB):")
print(f" curator base : {rec['model']}")
print(f" profile / window : {rec.get('profile')} / {rec.get('num_ctx')}"
f" (LLM_PROFILE / CURATOR_NUM_CTX, or PC_* for the PC)")
print(f" num_gpu : {rec.get('num_gpu')} (CURATOR_NUM_GPU)")
if rec.get("server"):
print(" Ollama server : " + ", ".join(f"{k}={v}" for k, v in rec["server"].items()))
if exp:
print(f" measured : {exp.get('gen_tok_s')} tok/s, verdict {exp.get('judge_s')} s, "
f"{exp.get('vram_gb')} GB VRAM")
print(" The bench ran on a card of its own: a card shared with other apps "
"(Plex transcodes, Immich) has less room than its size.")
return 0
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="Build or check Curatarr's Ollama bakes")
ap.add_argument("--check", action="store_true", help="check every host's models")
ap.add_argument("--host", choices=("main", "pc"), default="main",
help="which Ollama to build on (pc = OLLAMA_PC_ENDPOINT)")
ap.add_argument("--suggest", type=float, metavar="VRAM_GB",
help="print the bench's recommendation for a card this size")
args = ap.parse_args()
if args.check:
sys.exit(check())
if args.suggest is not None:
sys.exit(suggest(args.suggest))
asyncio.run(main(args.host))