Programmatic API#

Python clients: the same search pipeline the UI runs, callable from code.

Wheel exposes the same search pipeline that powers the web UI through two Python clients, so scripts and notebooks can build slices, run benchmarks, or upload artifacts without launching the web server locally.

Which Client#

The two differ in one respect: where the search actually runs.

WheelClient runs it in your own process, reading the indices, captions, and metadata straight off disk. Nothing has to be running first, but the script needs access to the data files.

WheelHTTPClient sends it to a Wheel server that is already running. You need its URL and a login, but none of the data files.

Everything else is the same. The method names, the arguments, and the results are identical, so a script written against one runs against the other by changing how the client is constructed.

WheelClient#

Point it at the same YAML the server reads and it runs queries directly.

from sil_wheel.client import WheelClient

# Load the stores listed in the YAML; no server is started
client = WheelClient.from_config("config/wheel_launch_dev_server_config.yaml")

# Keyword search over captions, returning the matching clips
result = client.search_caption("intersection")

# One row per clip, with its per-modality scores
df = result.as_dataframe()

# Any helper takes extra filters, so modes compose in a single call
result = client.search_classifier(
    "<classifier_run_id>",       # the run whose scores to filter on
    expression="p > 0.9",        # keep only high-confidence clips
    search_country="DE",         # and only clips recorded in Germany
)

WheelHTTPClient#

Point it at a running server. No data files and no YAML, just the URL and a login. Scores are not sent back over the wire, so use result.clip_ids.

from sil_wheel.http_client import WheelHTTPClient

# Log in once; the session is reused for every later call
client = WheelHTTPClient(
    server_url="http://wheel-host:8012",
    username="alice",
    password="...",
)

# Same call as the local client, executed on the server instead
result = client.search_caption("intersection")

# Full ranked list; scores are not returned over HTTP
clip_ids = result.clip_ids

Both clients also accept URLs copied straight from the browser address bar through search_from_url:

# Replay a search built in the UI, filters and all
result = client.search_from_url(
    "http://wheel-host:8012/?search=intersection&search_country=DE"
)

Both the hash-fragment form (http://host:port/#&search=...) and the plain query-string form (?search=...) are accepted, so any URL the UI produces can be replayed verbatim.

Search Helpers#

search(**kwargs) accepts every filter documented in Search URLs, and there is a named helper for each search mode: search_caption, search_caption_embed, search_semantic_text, search_clip, search_visual_text, search_visual_image, search_trajectory_pattern, search_trajectory_shape, search_classifier, search_cluster, search_country, search_clip_list, and search_world_model.

Every helper takes extra keyword arguments, so filters compose in a single call, as in the classifier example above.

Working With Results#

Every search returns the matching clips ranked by whichever mode is scoring them, following the priority order in Search when several are active:

result.clip_ids            # full ranked list
result.head(5)             # first 5
result.as_dataframe()      # one row per clip, with its per-modality scores

as_dataframe is the quickest way to check why clips ranked as they did, or to export a slice for downstream use. It carries scores only for WheelClient, since the server returns clip IDs alone.

Uploading Clustering Runs#

WheelHTTPClient ships an extra method, upload_clustering_run, for the offline-clustering workflow described in Cluster Search. Give it a directory holding a completed clustering run (cluster_assignments.parquet, representative_by_cluster.json, umap.json, metadata.json, and optionally cluster_topics.json) and it ships them to the server. The new run then shows up in the Clustering Tools panel exactly like an in-UI run.

# Ship a locally computed run to the server; overwrite=False keeps any
# existing run with the same ID
response = client.upload_clustering_run(run_dir, overwrite=False)

# The ID to open in the Clustering Tools panel, and what was written
print(response["run_id"], response["files_written"])

End-to-end runnable workflows live under examples/ in the wheel repository. examples/cluster_from_search.py is the canonical reference: it copies a search URL from the UI, runs k-means and UMAP locally against the FAISS embeddings, and uploads the resulting run back to the server.

Common Patterns#

  • Run a search from a script or notebook without copy-pasting filter values from the URL.

  • Bulk-export slices for downstream training or evaluation pipelines, writing the ranked clip IDs straight to disk or a parquet shard.

  • Reproduce a UI search in code by handing the browser URL to search_from_url, so a slice built interactively carries over into a script unchanged.

  • Scale clustering offline and push the result back through upload_clustering_run, letting you cluster runs that would not fit in the in-UI launcher’s capacity.