Skip to main content

renderdb

test

Your ComfyUI output folder is already a complete experiment record. This makes it queryable.

Every PNG ComfyUI writes carries the API graph that produced it — every seed, model, LoRA and strength. None of it is searchable, so in practice nobody looks. "When did I last run this LoRA at 0.4, and what came out" means opening files one at a time until you give up.

$ renderdb build ~/ComfyUI/output
indexed 1603, skipped 5 -> renders.db
  skipped     5  has no embedded workflow

$ renderdb sql "SELECT name, strength_model, COUNT(*) FROM render_lora
                WHERE strength_model < 0.5 GROUP BY name, strength_model"
$ renderdb diff a.png b.png
  seed         777000  ->  990001
  cfg          5.0  ->  6.0

  2 changes

No dependencies, stdlib only. 65 tests, CI on Python 3.9, 3.11 and 3.13.

The one field that is actually missing elsewhere

Be precise about the gap, because it is narrow.

infinite-image-browsing (1.3k stars, actively maintained) already parses LoRAs out of generation metadata and makes the name searchable. Its update_image_data.py does Tag.get_or_create(conn, i["name"], "lora") — and that is the whole of it. The strength is discarded on that line. Its image table is six columns wide: id, path, exif, size, date, exif_edited, with the entire metadata as one text blob.

ComfyUI_PromptManager (162 stars) stores workflow_data as a JSON blob. SDMeta (20 stars) caches metadata for display.

So: "LoRA is queryable" is taken. "At what strength" is not, and neither is asking it across a whole folder:

-- which LoRAs have I only ever run at full strength?
SELECT name FROM render_lora GROUP BY name HAVING MIN(strength_model) = 1.0;

-- everything from one pipeline, at one seed, sorted by when
SELECT file, cfg, steps FROM render
WHERE fingerprint = ? AND seed = 777000 ORDER BY mtime;

If a browser with a UI is what you want, use one of the above — this has no UI and never will.

Node shapes, not node ids, and the bug that proves why

Nothing here matches on a node id. The script this grew from located things by literal node number — 1043 is the quality prefix, 1130 the handwritten prompt — which works until ComfyUI renumbers the graph on save, and never worked on anyone else's workflow at all.

What it matches on is a shape: the input keys a family of nodes uses. Four ship:

shape looks like
native_loader stock LoraLoader: flat lora_name + strength_model
power_loader rgthree Power Lora Loader: lora_1, lora_2, … dicts
bundle LoRA-manager: a whole stack in one widget under __value__
inline_syntax A1111 text: <lora:name:0.8> inside a string field

The original understood only the bundle shape — because that is what its author's workflow used. Across 1,603 of their renders there is not one native LoraLoader, so nothing ever signalled the gap. Anyone on a stock ComfyUI install would have got LoRA names with no strengths and no error: the exact field the tool exists to provide, silently absent, on the most common setup in the ecosystem.

That is why LORA_READERS is a list you can append to:

from renderdb import workflow
workflow.LORA_READERS.insert(0, (my_predicate, my_reader))

An unrecognised shape is recorded as unresolved. It is never guessed at.

tests/fixtures/real_shapes.json holds seven graphs lifted from real renders — structure byte-for-byte as ComfyUI wrote it, names scrubbed — covering every LoRA node class the corpus contains, including an empty Power Lora Loader and the inline-syntax loader. They exist because a synthetic fixture can show that a new reader handles a new shape and can never show that it left the old ones alone, which is how three of the four corrections below became necessary.

Refuse per field, not per row

An index that drops a file is useless. One that stores a wrong value is worse. So every readable render gets a row — path, mtime and pipeline fingerprint are cheap and unambiguous — and only genuinely unreadable fields go NULL.

lora_status carries the distinction that a bare empty list destroys:

status meaning
ok every LoRA node was read
partial some were read; at least one shape had no reader
unsupported_node_shape LoRA nodes are present and none could be read
no_lora_nodes there are none — an empty list here is a fact

Without it, "this render used no LoRAs" and "I could not read this render's LoRAs" both look like zero rows, and every query counting LoRA usage is quietly wrong in a direction nobody notices.

Getting that status right took four corrections, every one found by running over 1,603 real files rather than by reasoning about it:

  • A LoRA-manager graph has one node holding the stack and several consumers wired to it — a loader, a trigger-word toggle — each carrying its own empty {"__value__": []} widget. Counting those as unread reported essentially every render as partial.
  • A trigger-word toggle with words toggled on carries a populated __value__ too, but its entries are {"text": ...} — words, not adapters.
  • A Power Lora Loader placed on the canvas and never filled in carries no LoRA, so it is not a LoRA that failed to read.
  • The one that mattered. 43 renders applied every one of their LoRAs as <lora:name:0.8> inside a plain text field — no filename, no __value__, no lora_name. They were reported no_lora_nodes: not a warning, not a partial, a confident there are none here. A silent false clean is the single failure this design exists to make impossible, and it was sitting in the author's own corpus the whole time.

The corpus now reports 1,447 ok, 156 no_lora_nodes, zero unreadable — 15,376 LoRA rows, every one with a strength.

The lesson is in the fourth one, and it is not "add another reader". A shape you have never seen looks exactly like an absence, so the honest posture is to assume the reader list is incomplete: lora_status exists because it will be, and the numbers above are what one corpus happens to contain, not a coverage claim.

What is deliberately not here

Prompt text is advisory. It is stored for search and kept out of the exact-match columns. The walk cannot see text a LoRA manager injects at run time or a prompt upsampler generates, so what it returns may be a fraction of what the model received. Good enough to grep; not good enough to compare on.

No tag table. The prototype split prompts on commas into 42,373 rows over 1,510 images. That heuristic breaks on natural-language and mixed prompts, and it duplicates the one part of the prior art that is genuinely well covered.

No prompt-source segmentation. The prototype could split a prompt into "quality prefix / injected trigger words / handwritten" — genuinely useful, and implemented as a lookup table of four literal node ids from one saved workflow. The API graph carries no node titles, so there is no general signal to reconstruct it from. Shipping it would have meant shipping a personal constant dressed as a feature.

A strength that could not be read is partial, never ok. A LoRA whose strength arrives as a wired-in link or a string leaves the column NULL. Calling that ok would claim "name and strength recovered" about the one column this package exists for.

Model identity is size and mtime, or None. A filename is not an identity: two people's anime_v3.safetensors are different files, and yours changes when you re-download it. Weak, but checkable — and None when the file is not found is an honest answer where a guess is not.

Commands

renderdb build <dir> [--db renders.db]     index a folder (prunes rows for
                                           files no longer there)
renderdb why <image.png> [--models <dir>]  how one render came to be
renderdb diff <a.png> <b.png>              what changed between two
renderdb stats [--db renders.db]           overview, LoRA usage, strength spans
renderdb sql "SELECT ..." [--db ...]       anything else

diff reports whether exactly one thing changed, and refuses to call a comparison single-variable when either render's LoRA stack could not be read — silence about what was unreadable is how a comparison gets trusted further than it should be.

Install

Not on PyPI. From a clone:

pip install .          # or -e ".[test]" to run the suite
pytest -q

See DESIGN.md for why it is shaped this way, what was rejected, and what to check before changing it.

License

MIT

Metadata

Release files for renderdb 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for renderdb 0.1.0
File Size Uploaded
renderdb-0.1.0.tar.gz 43.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for renderdb 0.1.0
File Interpreter ABI Platform
renderdb-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 64.6 kB

Release files / renderdb-0.1.0.tar.gz

Download URL renderdb-0.1.0.tar.gz
Size 43.7 kB
Tags Source
SHA-256 checksum
How to use checksums
81ec165acfa5db7e1d138f2ccdfdc662d6fb5ee40c5973474a5133825de1d06c
BLAKE2b-256 checksum
How to use checksums
22d5fc0034b9e793dca0c2240d8f17b48071e5919b8f2a1fba3bf67e988fe16a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.

Transparency log

Release files / renderdb-0.1.0-py3-none-any.whl

Download URL renderdb-0.1.0-py3-none-any.whl
Size 20.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5780719e4e3c2a23089578719509d669678ca00a9692f2f2e7d93cd300a86d26
BLAKE2b-256 checksum
How to use checksums
5b619f4cc5a8d434f5c7faed3550c1ec32364a0befd8f779f807999b0b2326c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page