LLMs still struggle to generate perfect vector images from a reference raster in one shot. vectrify turns raster images into editable vector code by treating vectorization as a search problem: an LLM proposes candidate SVG/Graphviz/Typst code, a vision scorer ranks how close each candidate looks to the source, and an optimization loop iteratively refines the best candidates.
The output is human-readable code you can keep editing by hand.
- Output formats: SVG (default), Graphviz DOT, Typst. HTML and TikZ planned.
- LLM providers: OpenAI, Anthropic, Google Gemini, auto-detected from env vars.
- Search strategies: NSGA-II for diversity-preserving multi-objective optimization, or beam search for a cheaper single-best run.
- Scoring: local vision-model embeddings (perceptual), with pixel-diff and LLM-as-judge as alternatives.
- Resumable runs: pick up where you left off, or fork from the top-N nodes of a previous run.
- Live dashboard: pool stats, scoring, and convergence criteria.
pipx or uv tool keeps vectrify in its own environment and on your PATH:
pipx install vectrify # or: uv tool install vectrify
pipx install "vectrify[vision]" # recommended for best quality
pipx install "vectrify[all]" # everythingPlain pip install works too, into whatever environment is active; when using --user, check that ~/.local/bin is on your PATH.
The base install covers SVG output and the pixel-difference scorer. The extras add the rest:
| Extra | What it adds |
|---|---|
| vision | torch + transformers for the perceptual (CLIP/SigLIP) scorer |
| graphviz | the graphviz Python bindings (system Graphviz still required) |
| typst | the typst Python compiler |
| all | vision + graphviz + typst |
Cairo is required for SVG output (apt install libcairo2 or brew install cairo), and the graphviz format additionally needs the Graphviz binaries (apt install graphviz or brew install graphviz). A GPU is optional; the vision scorer falls back to CPU or MPS.
Finally, set an API key for one provider:
export OPENAI_API_KEY=...
export ANTHROPIC_API_KEY=...
export GEMINI_API_KEY=...The provider is auto-detected from whichever key is set; override it with --provider {openai,anthropic,gemini} if you have several keys.
vectrify input.png -o output.svgThe defaults run up to 4 NSGA-II epochs and stop early once the search stops finding improvements (see Convergence and cost). Worst case, it runs for an hour and gives up.
A few useful variations:
# Bigger budget, longer runs
vectrify photo.jpg -o sketch.svg --epoch-patience 60 --max-wall-seconds 1800
# Steer the search with a goal
vectrify logo.png --goal "Use thick strokes only and avoid gradients"
# Output Graphviz DOT instead of SVG
vectrify diagram.png -o out.dot --format graphviz
# Resume from a previous run, keeping only the 20 best nodes
vectrify input.png --resume --resume-top 20Run vectrify --help for the full flag reference, organized into LLM provider, scoring, search strategy, epoch control, resume, output artifacts, and runtime sections.
vectrify runs an evolutionary loop over a pool of candidate vector representations. The pool is seeded with a few LLM-generated candidates. On each iteration a parent is sampled, and:
- with probability 1 − llm-rate, mutated locally (color tweaks, path nudges, crossover);
- otherwise, sent to the LLM for a refined edit.
The new candidate is scored against the source image (perceptual via vision-transformer embeddings, pixel-space, or LLM-as-judge), then either replaces a worse pool member or is dropped.
NSGA-II, the default, keeps a diverse Pareto front and does better when you can afford several epochs. Beam search converges faster on a single answer. NSGA-only flags are epoch-diversity, epoch-variance and epoch-seeds; beam-only flags are beams and cull-keep. The CLI rejects mixed usage.
The search minimizes three objectives at once:
| Objective | Measure |
|---|---|
| visual error | scorer distance to the source image |
| visual complexity | JPEG-compressed size of the render |
| structural complexity | code size (whitespace-stripped source length) |
Visual error is the primary objective; the complexity measures only break ties among the best-scoring candidates, biasing toward small, clean output once the image is already close. Raising tournament-size pushes harder toward visual quality at the cost of pool diversity.
Each epoch ends when one of these fires; the next re-seeds from the current Pareto front. The run stops at max-epochs, max-wall-seconds, or the max-llm-calls cap.
| Flag | Default | Triggers when… |
|---|---|---|
| max-epochs | 4 | hard cap on epoch count |
| epoch-patience | 20 | this many LLM calls in a row produce no improvement |
| epoch-steps | 50 | this many LLM calls have run in the current epoch |
| epoch-variance | 0 | (NSGA-only) score std-dev in the pool drops below value |
| epoch-diversity | 0 | (NSGA-only) mean pairwise diversity drops below value |
| max-wall-seconds | 3600 | wall-clock budget; ends the run, not just the epoch |
| max-llm-calls | 0 | hard cap on total LLM calls; 0 disables |
Patience and step counters only tick on LLM calls, not on the cheap local mutations that make up most tasks, and a new best resets patience. The two NSGA stop criteria are off by default; good thresholds depend on your scorer and image.
The defaults cap LLM calls near max_epochs × epoch_steps, so around 220, and most runs stop well before that. A full run costs on the order of a dollar on flagship models; set max-llm-calls for a hard ceiling.
Given --output sketch.svg, vectrify writes:
sketch.svg # the best final candidate (written at the end)
sketch/
└── runs/
└── 2026-04-26_14-30-21/ # one directory per run, timestamped
├── lineage.csv # accepted node history (all three objectives, parent, ops)
└── nodes/
├── 0.0421_0001.svg # one file per accepted node, prefixed by score
├── 0.0421_0001.png # rendered preview (--save-raster)
└── ...
Disable artifacts you don't need with no-write-lineage or no-save-raster. Enable save-heatmap to also dump perceptual diff maps next to each node.