Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

eligo

eligo — generate many images, keep the best; best-of-N selection in pure Rust

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.

crates.iodocs.rsCILicenseMSRV


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:

  1. Generatesn candidate images (the artist — a pluggable Backend).
  2. Scores each one against the prompt (the judge — a pluggable Scorer that returns a number; higher is better).
  3. Selects the highest-scoring candidate and returns it.

eligo selection loop: a prompt feeds the Backend (the artist), which produces n candidate images; the Scorer (the judge) gives each a reward; argmax picks the winner

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

crates/
eligo/ # library: the loop, traits, scorers, embedder
eligo-cli/ # binary (clap), installs as `eligo`

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:

FeatureAddsRuntime
(default)mock backend + mock scorernone
clipClipScorer (the real judge) + ClipEmbedderONNX Runtime (ort)
sdSdBackend — Stable Diffusion txt2img (the real artist)ONNX Runtime (ort)

Quick start

just build
just test# Mock loop — no models, instant. Generate 5, keep the best, write the winner:
just run -- generate "a lighthouse at dusk" -n 5 --reroll-worst --out winner.ppm

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:

cargo run -p eligo-cli --features "sd clip" -- generate \
"a photograph of a red apple on a wooden table" -n 4 --steps 20 \
--sd-model-dir <sd-onnx-dir> --sd-tokenizer <tokenizer.json> \
--clip-model <clip.onnx> --clip-tokenizer <tokenizer.json> \
--out winner.png --save-all
  • --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 eligo::{ClipScorer,QualityWeighted};let clip = ClipScorer::from_files("clip.onnx","tokenizer.json")?;// 70% prompt-match, 30% image quality:let scorer = QualityWeighted::new(Box::new(clip),0.3);

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:

cargo run -p eligo-cli --features clip -- similar \
query.png ./photos -k 5 \
--clip-model clip.onnx --clip-tokenizer tokenizer.json
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.pubtraitBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>;}/// The judge: prompt + image → reward (higher is better).pubtraitScorer{fnscore(&self,prompt:&str,image:&Image) -> Result<f32>;}pubfnbest_of_n(backend:&dynBackend,scorer:&dynScorer,cfg:&GenerateConfig)
-> Result<Selection>;
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 rewardswrap with QualityWeighted, or write a composing Scorer
Build "more like this", dedup, or searchuse ClipEmbedder::embed_image + cosine_similarity
Power a recommendation engine / media catalogueembed assets once, store the vectors, do nearest-neighbour lookups outside eligo
Add a new no-reference metricsit 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 eligo::{Backend,Image,Result};structMyApiBackend{client:MyClient}implBackendforMyApiBackend{fngenerate(&self,prompt:&str,seed:u64) -> Result<Image>{let pixels = self.client.txt2img(prompt, seed)?;// your model / serviceImage::new(width, height, pixels)// RGB8, row-major}}

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 eligo::{Scorer,Image,Result};/// Prefer images that are mostly *not* dark.structPreferBright;implScorerforPreferBright{fnscore(&self,_prompt:&str,image:&Image) -> Result<f32>{let mean = image.rgb.iter().map(|&b| b asf32).sum::<f32>()
/ image.rgb.len()asf32;Ok(mean / 255.0)// 0..1, brighter = higher}}

Drop either into the same loop:

use eligo::{best_of_n,GenerateConfig};let selection = best_of_n(&MyApiBackend{ .. },&PreferBright,&GenerateConfig::new("a sunset"))?;println!("winner seed = {}", selection.best().seed);

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 eligo::{ClipEmbedder, cosine_similarity};let embedder = ClipEmbedder::from_files("clip.onnx","tokenizer.json")?;let a = embedder.embed_image(&img_a)?;// image → L2-normalized vectorlet b = embedder.embed_image(&img_b)?;let how_alike = cosine_similarity(&a,&b);// 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

ItemUse
ImageRGB8 buffer; Image::open / Image::save_png (with clip/sd)
cosine_similarity, l2_normalizevector math for any embedding
quality_score / QualityScorerno-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.

TaskCommand
Formatjust fmt
Lintjust lint
Testjust test
Test a featurecargo test -p eligo --features clip
Docsjust docs
Dependency auditjust 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

  1. Update CHANGELOG.md under a new ## [x.y.z] heading and commit.
  2. just release x.y.z — bumps versions, tags, and pushes.
  3. CI builds binaries for macOS (arm64 + x86_64), Linux, and Windows, and publishes a GitHub Release with checksums and the changelog notes.
  4. 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.

About

Generate many images, keep the best — best-of-N image selection in pure Rust

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages