Selecting images
Pulling data takes three steps: discover a benchmark's dimensions with /catalog, select the items you want with /items, then fetch each with /download. Selection happens entirely in the /items query string, so a request fully describes the set you are pulling. The recipes below are copy-ready against synthetic-face-v1.
Two rules cover every query: a comma-separated value matches any of the listed values (OR within a dimension), and separate parameters must all hold (AND across dimensions). Omit a dimension to include all of its values.
Two terms used throughout: a cell is one skin-tone x gender combination, and a lineage is a sourced real image plus every fake and perturbed variant derived from it, all sharing one source_real_id.
iter_items and download_selection, which live in the margen.ergonomics submodule. Import them once per session:from margen.ergonomics import iter_items, download_selectionOne specific type
The finest-grained selection: a single cell, one condition. Every filter is a single value, so exactly one type of image comes back.
Request
1items = list(iter_items(2client, benchmark="synthetic-face-v1",3kind="fake", skin_tone="dark", gender="female", perturbation="clean",4))
Response
1{2"object": "list",3"total": 12,4"has_more": true,5"benchmark": "synthetic-face-v1",6"data": [7{ "object": "attack_data_item", "id": "8f3c1d2e-...",8"kind": "fake", "skin_tone": "dark", "gender": "female",9"generator": "diffusion-v1", "perturbation": "clean",10"layer": "clean", "scene": "selfie",11"base_id": "b2d4e6f8-...", "source_real_id": "real_0001" }12]13}
Several values at once
Pass a Python list to build a set in one call: dark or brown, at JPEG q70 or q80, that are fakes. The helpers (iter_items / iter_lineages) accept a list or a comma-separated string; either way the comma form goes on the wire (the same as the curl query). applied_filters echoes exactly what the query understood.
Request
1items = list(iter_items(2client, benchmark="synthetic-face-v1", kind="fake",3skin_tone=["dark", "brown"], perturbation=["jpeg_q70", "jpeg_q80"],4))
Response
1{2"object": "list",3"total": 64,4"has_more": false,5"benchmark": "synthetic-face-v1",6"applied_filters": {7"skin_tone": ["dark","brown"], "kind": ["fake"],8"perturbation": ["jpeg_q70","jpeg_q80"], "gender": null9},10"data": [ /* dark+brown x q70+q80 fakes */ ]11}
A matched lineage (real + everything from it)
Pull a sourced real image and every fake and perturbed variant derived from it, all sharing one source_real_id. Use this to build matched real/fake pairs for paired evaluation. To page over whole lineages at once, add lineage="true".
Request
1items = list(iter_items(2client, benchmark="synthetic-face-v1", source_real_id="real_0001",3))
Response
1{2"object": "list",3"benchmark": "synthetic-face-v1",4"data": [5{ "object": "attack_data_item", "id": "r1a2b3c4-...", "kind": "real",6"perturbation": "clean", "scene": null,7"base_id": "img_r1a2", "source_real_id": "real_0001" },8{ "object": "attack_data_item", "id": "f5d6e7f8-...", "kind": "fake",9"perturbation": "clean", "generator": "diffusion-v1",10"scene": "selfie",11"base_id": "img_f5d6", "source_real_id": "real_0001" },12{ "object": "attack_data_item", "id": "f9a0b1c2-...", "kind": "fake",13"perturbation": "fb_pipeline", "generator": "diffusion-v1",14"scene": "selfie",15"base_id": "img_f5d6", "source_real_id": "real_0001" }16]17}
Other conditions of the SAME image
Hold an item's base_id and change perturbation to pull another condition of the exact same image. Every perturbation of one image shares a base_id (distinct from source_real_id, which spans the whole real-source family).
Request
1item = items[0] # any item you already pulled above2# pull every perturbation of that same base image3variants = list(iter_items(4client, benchmark="synthetic-face-v1", base_id=item.base_id,5))6# or jump straight to one condition of the same image:7one = client.list_items(8benchmark="synthetic-face-v1", base_id=item.base_id, perturbation="jpeg_q70",9).result.data[0]
Response
1{2"object": "list",3"benchmark": "synthetic-face-v1",4"data": [5{ "object": "attack_data_item", "id": "f7c8d9e0-...", "kind": "fake",6"perturbation": "jpeg_q70", "generator": "diffusion-v1",7"base_id": "img_f5d6", "source_real_id": "real_0001" }8]9}
One image per person (dedupe by identity)
identity_id is the reconciled person, broader than source_real_id(one source image's family): a real and its identity-preserving fakes share it. Pass distinct_identities="true" for one item per person (opt-in, no default cap; composes with filters, so kind="real" gives one real per person), or dedupe an already-pulled list client-side with unique_identities / group_by_identity.
Request
1# every item carries identity_id (the reconciled PERSON, spanning all2# of that person's source images). Dedupe by person two ways:34# server-side: one representative item per identity (composes with filters)5people = list(iter_distinct_identities(6client, benchmark="synthetic-face-v1", kind="real",7))89# or client-side on an already-pulled list (cap N per person, optional seed):10items = list(iter_items(client, benchmark="synthetic-face-v1"))11one_each = unique_identities(items, n=1)12by_person = group_by_identity(items) # {identity_id: [items...]}
Response
1{2"object": "list",3"benchmark": "synthetic-face-v1",4"mode": "distinct_identities",5"total_identities": 241,6"data": [7{ "object": "attack_data_item", "id": "r1a2b3c4-...", "kind": "real",8"identity_id": "9f2c-...", "source_real_id": "real_0001" }9]10}
By scene (synthetic only)
Filter on scene to select synthetic faces by semantic context: indoor, outdoor, or selfie. Like skin_tone it takes a comma list (or a Python list, which the helpers send as the comma form). scene applies to synthetic images only: real images always carry scene: null, so filtering by scene alone restricts the result to fakes, and combining scene=... with kind=real is empty by construction.
Request
1# scene labels the semantic context of a synthetic face2items = list(iter_items(3client, benchmark="synthetic-face-v1",4kind="fake", skin_tone="dark", scene=["indoor", "selfie"],5))
Response
1{2"object": "list",3"benchmark": "synthetic-face-v1",4"data": [5{ "object": "attack_data_item", "id": "8f3c1d2e-...",6"kind": "fake", "skin_tone": "dark", "gender": "female",7"generator": "diffusion-v1", "perturbation": "clean", "layer": "clean",8"scene": "selfie",9"base_id": "b2d4e6f8-...", "source_real_id": "real_0001" }10]11}
Pulling metadata
Labels are free. /itemsreturns each image's labels at no cost. No credit is spent browsing, so you can survey the full label set of a selection before pulling a single byte. The only thing that costs a credit is /download, which pays for the image bytes. That split lets you filter, count, and inspect labels for the whole corpus for free, then spend credits only on the images you actually want.
By default /items returns the lean selection fields (ids and core dimensions). Pass ?include=metadata to attach the full per-image label object under metadata on every item. /download takes the same include=metadata and returns metadata on its item. The object is flat and stable, the same keys on every row, so a streamed result builds a consistent table (declare your columns once, append a row per item). The field list, types, and nullability are published by /catalog, so a client can declare its table columns before streaming.
Attach the full label set
Pass include=metadata to attach the full, flat label object to every item, for free. Every item carries the exact same key set, the full field list from /catalog, with null where a label is absent (the object above is abbreviated). That stability is the point: declare your columns once and append one row per item to build a consistent table. Trim client-side to the columns you want.
Request
1# labels are free: pull the whole label object per item, no credit spent2items = list(iter_items(3client, benchmark="synthetic-face-v1",4kind="fake", skin_tone="dark", include=["metadata"],5))6row = items[0].metadata # flat, stable keys -> one table row per item
Response
1{2"object": "list",3"benchmark": "synthetic-face-v1",4"data": [5{ "object": "attack_data_item", "id": "8f3c1d2e-...",6"kind": "fake", "skin_tone": "dark", "gender": "female",7"generator": "diffusion-v1", "perturbation": "clean",8"layer": "clean", "base_id": "b2d4e6f8-...", "source_real_id": "real_0001",9"metadata": {10"skin_tone_gemini": "dark",11"skin_tone_ita": 12.4,12"gender_presentation": "female",13"age_bucket": "25-34",14"scene_lighting": "even",15"face_occlusion": "none",16"difficulty": "hard",17"crop_width_px": 51218// ...every /catalog field is present; null where a label is absent19} }20]21}
Fetching and paging
Fetch each item with GET /api/v1/data/download/<id>. It returns a signed URL that delivers one JPEG image and expires after 300 seconds (5 minutes), so fetch it promptly and send no auth header on that request. On the live tier downloads are credit-metered; check your balance first with /api/v1/data/usage to avoid a mid-run 402.
There are three ways to page, depending on how large the pull is and how you use it:
offset+limitfor one-shot small pulls (returns an exacttotal, the number of images matching your filter).cursorfor large or repeated pulls: stable if items are added while you page (pass the responsenext_cursorback in). This is the only mode a generated SDK auto-pages; offset and lineage modes are paged manually.lineage=trueto page by matched sets rather than rows (each page is whole lineages).
total is the exact number of images matching your query, the whole benchmark with no filters, or the size of a specific class when you filter (e.g. kind=fake&skin_tone=dark&gender=female gives that cell's size). Counting and browsing are free, no credits, so pair any filter with limit=1 to read a class size without pulling rows. What you can actually download is bounded by your credit balance (1 credit = 1 unique image), not by total. Everyone can see the full benchmark; credits are the only pull limit.Download a batch to your machine
Set N and stream that many images to a local folder in one call. download_selection pulls each image's bytes to out_dir (streamed to disk in chunks), debits one credit per unique image (the test tier is free), and stops cleanly if you run out of credits, returning the paths it saved. itertools.islice caps the selection at N, so exactly N images are pulled and N credits spent. In curl it is the same two steps in a loop: list ids, then fetch each signed URL. The response shown is one download; the SDK repeats it for every item under the hood.
Request
1import itertools # the only new import; client + helpers are set up above23N = 100 # how many images to pull45picks = iter_items(client, benchmark="synthetic-face-v1", kind="fake", skin_tone="dark")6saved = download_selection(client, itertools.islice(picks, N), out_dir="out/")7print(f"saved {len(saved)} images") # -> ['out/dark_female_fake_clean_8f3c1d2e.jpg', ...]
Response
1{2"object": "download",3"url": "https://...supabase.co/storage/v1/object/sign/...",4"expires_in": 300,5"item": { "object": "attack_data_item", "id": "8f3c1d2e-...",6"kind": "fake", "skin_tone": "dark", "gender": "female",7"base_id": "b2d4e6f8-..." },8"balance": 4900,9"charged": true,10"already_owned": false11}
Re-run without re-pulling (only new images)
Pass exclude_owned=True to pull only images you do not already own. Credits are per unique image (own-once), so re-downloading one you already paid for is free, but it still costs time to fetch the bytes; this skips them at selection time, so a re-run on an instance downloads only what is new. When you own the whole matching subset the response returns remaining: 0 and subset_exhausted: true with a message, and download_selection saves nothing. It also reports owned and total_matching so you can see your coverage. A re-download that does happen prints (already owned, no charge).
Request
1# re-runnable: pull only images this account does not already own.2# Safe to run again (e.g. after topping up credits); owned images are skipped.3new = iter_items(4client, benchmark="synthetic-face-v1", kind="fake", skin_tone="dark",5exclude_owned=True,6)7saved = download_selection(client, new, out_dir="out/")8print(f"saved {len(saved)} new images") # 0 on a re-run once you own them all
Response
1{2"object": "list",3"benchmark": "synthetic-face-v1",4"exclude_owned": true,5"remaining": 0,6"owned": 342,7"total_matching": 342,8"subset_exhausted": true,9"message": "You already own every image matching this filter.",10"data": []11}