Quickstart
Create a key in the portal, then pull with the SDK (pip install margen).
1import os, urllib.request2from margen import Margen34client = Margen(bearer_auth="mgn_test_...") # free test key from the portal56# The passport test set is a free sample: one real and one fake per demographic7# cell, from both editors (see Benchmarks). Pull one HiDream fake:8item = client.list_items(9benchmark="passport-pad-v1", kind="fake", generator="hidream_o1", limit=1,10).result.data[0]1112dl = client.download_item(item_id=item.id) # test tier: free, no credit debited13print("signed url:", dl.url) # click to open the image in a browser1415out_dir = "margen_out" # <- change to any folder you like16os.makedirs(out_dir, exist_ok=True)17path = os.path.join(out_dir, f"{item.id}.jpg")18urllib.request.urlretrieve(dl.url, path) # signed URL, no auth header19print("saved to:", path)
Pull the whole dataset
One script that mirrors what you would get from a HuggingFace repo: every image plus a metadata.csv of the full label set. It is resumable, so a run that stops at a zero balance can be re-run to fill the gap without paying twice for what you already hold.
NOTE
Raise your key's rate limit before a full pull. Every image is one API call, so the limit sets the floor on how long the run takes. At the default
60/min a 159,672-image benchmark would take roughly 47 hours; at the maximum 600/min it is about 5 hours. Set it when you create the key, or edit it in the portal. The script preflights this and refuses to start if the limit would make the pull take more than eight hours, because a rate-limited download is skipped rather than retried: too low a limit produces a run that looks successful but fetches only part of the set.Python
1"""Pull a whole benchmark to a local folder, loadable exactly like a HuggingFace dataset."""2import csv, os3from margen import Margen4from margen.ergonomics import iter_items, download_selection56BENCHMARK = "passport-pad-v1"7OUT = "margen_passport"89client = Margen(bearer_auth=os.environ["MARGEN_API_KEY"])10os.makedirs(OUT, exist_ok=True)1112# 1. Catalogue every item with its full label object. Browsing labels is FREE:13# no bytes leave the server, no credits are debited. You get the whole table14# even if you never download an image.15catalog = list(iter_items(client, benchmark=BENCHMARK, include="metadata"))16print(f"catalogue: {len(catalog)} items")1718# 1b. PREFLIGHT. Every image is one API call, so the key's rate limit sets the floor19# on wall-clock. A 429 is treated as skippable by the downloader, which means a20# limit that is too low does NOT fail loudly: the run finishes having fetched a21# fraction of the set. Check first and refuse to start.22u = client.get_usage().result23per_min = getattr(u, "rate_limit_per_min", 60) or 6024hours = len(catalog) / per_min / 6025if hours > 8:26raise SystemExit(27f"{len(catalog)} images at {per_min}/min would take {hours:.1f}h.28"29f"Raise this key's rate limit in the portal (max 600/min, "30f"about {len(catalog) / 600 / 60:.1f}h) before starting."31)32if u.balance < len(catalog):33print(f"[warn] balance {u.balance} < {len(catalog)} images; "34f"the pull will stop when credits run out (re-run after topping up)")35print(f"rate limit {per_min}/min, estimated {hours:.1f}h")3637# 2. Download only what this account does not already hold. Re-running is safe38# and cheap: owned images are skipped, so after topping up credits you just39# run it again and it fills the gap. Stops cleanly at a zero balance.40todo = list(iter_items(client, benchmark=BENCHMARK, exclude_owned=True))41print(f"to fetch: {len(todo)} (already owned: {len(catalog) - len(todo)})")42download_selection(client, todo, OUT)4344# 3. Write metadata.csv describing whatever is ACTUALLY on disk, including files45# from earlier runs. Matching on the id suffix rather than recomputing the46# filename means this stays correct if the naming convention ever changes.47on_disk = {}48for fn in os.listdir(OUT):49if fn.endswith(".jpg"):50on_disk[fn[:-4].rsplit("_", 1)[-1]] = fn5152def val(v):53return getattr(v, "value", v) # enum -> its value5455def row_for(it, filename):56md = getattr(it, "metadata", None) or {}57if not isinstance(md, dict):58md = {k: v for k, v in vars(md).items() if not k.startswith("_")}59row = {60# file_name FIRST: this is what makes the folder a HuggingFace imagefolder.61"file_name": filename,62"label": 1 if val(getattr(it, "kind", None)) == "fake" else 0,63"id": str(it.id),64"kind": val(getattr(it, "kind", None)),65"skin_tone": val(getattr(it, "skin_tone", None)),66"gender": val(getattr(it, "gender", None)),67"generator": val(getattr(it, "generator", None)),68"perturbation": val(getattr(it, "perturbation", None)) or "clean",69# identity_id groups images of the SAME PERSON. Cluster your confidence70# intervals on this, not on row count, or they will be optimistic.71"identity_id": getattr(it, "identity_id", None),72"source_real_id": getattr(it, "source_real_id", None),73}74row.update({f"meta_{k}": v for k, v in md.items()})75return row7677rows = [row_for(it, on_disk[str(it.id)[:8]]) for it in catalog if str(it.id)[:8] in on_disk]78cols = list(dict.fromkeys(k for r in rows for k in r))79with open(os.path.join(OUT, "metadata.csv"), "w", newline="") as fh:80w = csv.DictWriter(fh, fieldnames=cols, extrasaction="ignore")81w.writeheader()82w.writerows(rows)8384print(f"{len(rows)} images + metadata.csv -> {OUT}/")85print(f'load it: datasets.load_dataset("imagefolder", data_dir="{OUT}")')
The output folder loads directly with datasets.load_dataset("imagefolder", data_dir=...), so anything written against a HuggingFace dataset works unchanged. Browsing labels costs nothing: only /download debits a credit, so you can pull the entire label table first and decide what is worth fetching. Cluster any confidence interval on identity_id rather than row count, since one person contributes several images.