eligo

Name: eligo is Latin for "I choose / I pick out" — the root of elect and elite. It names the tool's one job: out of many candidate images, elect the best one.
What it does
Image generators are random — every attempt comes out different, some good, some junk. The usual fix is to make several and let a human pick. eligo automates the picking.
Give it a prompt and it:
- Generates
ncandidate images (the artist — a pluggableBackend). - Scores each one against the prompt (the judge — a pluggable
Scorerthat returns a number; higher is better). - Selects the highest-scoring candidate and returns it.
That generate → score → select loop is the smallest honest agentic pattern: a numeric reward drives a decision. An optional bounded re-roll regenerates the single worst candidate once — and that's the only loop; there is no open-ended "keep refining."
Scope is deliberately bounded. eligo owns the loop and the two contracts
(Backend, Scorer). It is not a model zoo, not an editor, and not a
recommendation service — but its parts are the foundation you build those on
(see Extending eligo).
Install / layout
The default build needs no AI models and no native runtime — it ships a deterministic mock backend and scorer so the whole loop runs and tests green out of the box. Real models are opt-in cargo features:
| Feature | Adds | Runtime |
|---|---|---|
| (default) | mock backend + mock scorer | none |
clip |
ClipScorer (the real judge) + ClipEmbedder |
ONNX Runtime (ort) |
sd |
SdBackend — Stable Diffusion txt2img (the real artist) |
ONNX Runtime (ort) |
Quick start
# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
The CLI has two subcommands: generate (best-of-N) and similar
(find look-alike images). If you don't have just,
cargo install just or use the cargo run -p eligo-cli -- … forms below.
The full thing: real images, real selection
With both features on, eligo generates actual Stable Diffusion images and keeps the one CLIP judges best:
--features sd— the artist (SdBackend): turns the prompt into images.--features clip— the judge (ClipScorer): scores each against the prompt.--quality-weight 0.3— also reward sharp, clean images (see below).--save-all— write every candidate, not just the winner, so you can see what the judge chose between.
Models are standard ONNX exports: a diffusers text_encoder / unet /
vae_decoder directory for SD, and a CLIP model.onnx + tokenizer.json. Both
are validated end-to-end in crates/eligo/tests/{sd_real,clip_real}.rs (ignored
by default; pointed at weights via env vars).
Scoring beyond the prompt: no-reference quality
CLIP answers "does this match the words?" — but a blurry image can still match
the words. The quality signal (always in the core, no model needed) answers
"does it look good?" using sharpness + contrast. Blend the two:
use ;
let clip = from_files?;
// 70% prompt-match, 30% image quality:
let scorer = new;
On the CLI that's --quality-weight 0.3. Raising it makes eligo prefer crisp,
detailed candidates even at a slight cost to literal prompt-match.
Find similar images (similar)
The same CLIP embeddings that judge prompt-match also measure image↔image
similarity — the basis for "more like this", dedup, and content-based
recommendations. ClipEmbedder::embed_image turns an image into a vector;
nearby vectors are look-alikes. The similar subcommand ranks a folder against
a query image:
most similar to query.png:
1.0000 ./photos/query.png # itself
0.9238 ./photos/other_a.png
0.9101 ./photos/other_b.png
Extending eligo
eligo is built around two small traits. Everything else — real models, quality blending, similarity — is an implementation of one of them, or a reuse of the embedder. Here is the whole surface you extend against:
/// The artist: prompt + seed → image.
/// The judge: prompt + image → reward (higher is better).
;
| You want to… | Do this |
|---|---|
| Use a different generator (SDXL, Flux, a diffusion API, even a non-AI renderer) | implement Backend |
| Change what "best" means (aesthetics, face presence, brand-safety, NSFW filter, OCR legibility, palette) | implement Scorer |
| Combine several rewards | wrap with QualityWeighted, or write a composing Scorer |
| Build "more like this", dedup, or search | use ClipEmbedder::embed_image + cosine_similarity |
| Power a recommendation engine / media catalogue | embed assets once, store the vectors, do nearest-neighbour lookups outside eligo |
| Add a new no-reference metric | sit it next to quality_score and blend it in |
1. A custom backend (your own artist)
Return an RGB8 [Image]; the loop handles seeding (candidate i gets
seed + i) and selection for you.
use ;
2. A custom scorer (your own definition of "best")
Anything you can turn into a number is a reward. The prompt is provided in case you want it; ignore it for prompt-independent rewards.
use ;
/// Prefer images that are mostly *not* dark.
;
Drop either into the same loop:
use ;
let selection = best_of_n?;
println!;
3. Similarity & recommendations (reuse the embedder)
ClipEmbedder is factored out so you can use the embeddings directly — no need
to go through the scorer:
use ;
let embedder = from_files?;
let a = embedder.embed_image?; // image → L2-normalized vector
let b = embedder.embed_image?;
let how_alike = cosine_similarity; // in [-1, 1]
// or: embedder.image_similarity(&img_a, &img_b)?
To build a recommender on top, the clean split is: eligo provides the embedding and the similarity math; the consuming catalogue embeds each asset once, stores the vectors, keeps a nearest-neighbour index (brute-force cosine for thousands; an HNSW index for tens of thousands+), and adds any per-user signals. Storage, indexing, and personalization stay out of eligo so it remains a focused selection library.
Reusable parts
| Item | Use |
|---|---|
Image |
RGB8 buffer; Image::open / Image::save_png (with clip/sd) |
cosine_similarity, l2_normalize |
vector math for any embedding |
quality_score / QualityScorer |
no-reference sharpness/contrast quality |
mock::{MockBackend, MockScorer} |
deterministic stand-ins for tests |
Development
just check-all runs the exact gate CI enforces — formatting, clippy
(-D warnings), tests, and docs — before you push.
| Task | Command |
|---|---|
| Format | just fmt |
| Lint | just lint |
| Test | just test |
| Test a feature | cargo test -p eligo --features clip |
| Docs | just docs |
| Dependency audit | just deny (needs cargo install cargo-deny) |
See docs/ROADMAP.md for the milestone history (M0 loop → M1
judge → M2 artist → M3 quality → M4 embeddings/similarity) and the explicit
non-goals.
Releasing
- Update
CHANGELOG.mdunder a new## [x.y.z]heading and commit. just release x.y.z— bumps versions, tags, and pushes.- CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
- To also publish to crates.io:
PUBLISH=1 just release x.y.z.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.