Skip to content

About

Zero-shot text search over 3D-printable miniature STLs: headless renders, pose resolution, SigLIP embeddings. Python · PyTorch

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

324 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mini-classify

Zero-shot search over a collection of printable-miniature STLs. No labels, no training: each mesh is rendered headless, its up-axis resolved, the views embedded with SigLIP, and then any text query — or any other model — is scored against those embeddings.

Ask it for "wizard with a staff" and it finds one, in a library where the filenames are things like 32_Unsupported_Renard_BodyNoMask.stl.

What it produces

The product is the caches, not the CSV. A classify run writes, per model:

  • a resolved pose (which way is up, and which view is the front),
  • per-view embeddings as .npy,
  • optionally the renders themselves.

Everything else reads those. results.csv is a thin top-3 dump; the real querying happens in the REPL or over the HTTP API, both of which are milliseconds per query once the caches exist because no rendering happens again.

Requirements

  • Python 3.12, a .venv managed with uv (uv pip install --python .venv/bin/python <pkg>).
  • A GPU for SigLIP (CUDA if present, CPU otherwise) and a working headless Open3D/Filament for rendering. On the development machine those are two different devices — rendering on an AMD iGPU, SigLIP on an RTX 4060 — which is a measured win, not a requirement.
  • Somewhere with STL files. The library is not in the repo.

Use

# 1. build the caches (the slow part: renders, poses, embeddings)
.venv/bin/python classify_stls.py /path/to/stls --cache-dir embed-cache

# 2. query interactively
.venv/bin/python test_categories.py --cache-dir embed-cache

# 3. or serve the same queries over HTTP
.venv/bin/python serve_api.py --cache-dir embed-cache --port 8077

After the first run, the cache-identity flags (--views, --elevations, --model, …) default to what that run recorded, so later commands usually need only --cache-dir.

The REPL

<enter> reloads categories.txt and classifies everything; any other text is a one-off query; :find, :pool, :min and :raw adjust the search. Results are clickable links to the model's render.

The API

Start it before anything that consumes it — nothing launches it for you, and a consumer polling http://127.0.0.1:8077/status sees a connection refusal until you do:

.venv/bin/python serve_api.py --cache-dir embed-cache --port 8077

It answers /status immediately with ready: false and serves queries once SigLIP is resident (16.0 s measured against embed-cache2, 2801 models, SigLIP on the 4060 — the model load is nearly all of it; LEARNINGS, 2026-08-19); /query, /similar, /poses and /under return 503 in between, so a consumer can tell warming from not-running.

Six routes over the same code the REPL uses — GET /status, POST /query, POST /similar, POST /poses, POST /under, POST /reload. It is loopback-only and unauthenticated by design: the intended caller is another local service, not a browser.

/poses is the one that carries no search: paths in, the same pose block a hit carries out (null for anything this index has not walked), so a consumer listing a directory can orient models it never searched for. It is a lookup in the pose cache — no GPU, no lock.

/under is the step before that: a directory in, every model this index holds beneath it with its pose out, sorted by root-relative path and cut at a required limit. It answers a folder's contact sheet from the store, so a consumer whose own walk cannot find the posed kits does not have to walk at all — the same no-GPU, no-lock, no-per-model-I/O budget /poses keeps.

/query takes a path to search within, and reports what it could not cover — a directory that exists but has never been classified answers 200 with status: "unindexed" rather than looking like "nothing matched". Full spec in docs/api/surface.md; it generates its own OpenAPI document at /openapi.json.

Other tools

cluster_models.py k-means over the cached embeddings — discover groupings without categories
migrate_cache_keys.py re-key a cache written under an older scheme, instead of re-rendering it
unpack_models.py some sets ship per-model zips inside the download, so the walk sees no STLs and the whole set is silently absent; this extracts them
eval/ measurement harnesses; every number in LEARNINGS.md came from one

Reading the repo

The docs are load-bearing here — most of the non-obvious decisions were measured rather than chosen, and the measurement is written down.

  • CLAUDE.md — the orientation document, and the constraints that cost real time to discover.
  • LEARNINGS.md — index of dated write-ups; every measured number resolves through it.
  • OPEN_QUESTIONS.md — open work, amended in place with answers rather than deleted.
  • docs/actor-refactor/interfaces.md — the live spec of the pipeline: what each module may import, and why.
  • docs/api/ — the query API's spec and its phased plan.
  • docs/cache-rebuild.md — debt that only a full cache rebuild can pay off. Read before rebuilding.

Tests

.venv/bin/python -m pytest tests/ -q

The renderer is faked at the op-log level and the API is exercised against a stub embedder, so the bulk needs no GPU and no model download. The few tests that do need something — CUDA, a local ollama, a 7z binary — skip when it is absent rather than failing.

About

Zero-shot text search over 3D-printable miniature STLs: headless renders, pose resolution, SigLIP embeddings. Python · PyTorch

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages