datasets volume changes, and the dataset stays annotatable afterwards.
A decision dataset is the exception. It holds no recordings and no transcript manifest, so it is not
published to a bucket: download its gold set instead (Decision gold sets).
There are two kinds of destination, and the difference is only where the credential lives.
A bucket Levers operates — nothing to set up
Pick it and publish. No key, no secret, no first-time setup: the dashboard holds the credential and signs one short-lived upload URL per object, and the pipeline container uploads straight to Cloud Storage with those. A platform admin configures which buckets appear (LEVERS9_PUBLISH_DESTINATIONS on the dashboard).
This is deliberate, not just convenient. A workspace secret is readable by whoever owns that workspace — they
can attach it to a job of their own and print it — so a key for a bucket holding every tenant’s training data
must not live there. What the container gets instead is a ticket scoped to one dataset’s folder: it cannot
write to another dataset, cannot climb out of its folder, and stops working when the run is over.
--folder
(or in the Publish dialog). Folders published before owners were recorded went to the workspace that published
them first, and a platform admin can hand a folder to another workspace from Admin → unowned.
The tickets are signed with the dashboard’s LEVERS9_AUTH_SECRET. A dashboard without one (a local one with
sign-in off) offers no shared buckets at all, and says so if you ask for one.
Your own bucket — a key you control
For a client’s project, or anywhere Levers does not operate the bucket, the credential is a service-account key stored as a workspace secret. It is read inside the pipeline container that does the upload, never by the dashboard, and never written to the dataset’s config — the config records only which secret to read. The rest of this page is that path.1. A service account that may write to the bucket
Give it object access to that one bucket, nothing else:objectAdmin rather than objectCreator because re-publishing overwrites the manifest. If nothing may ever be
overwritten, use objectCreator and publish with a fresh folder name each time.
2. The key as a workspace secret
The whole key file is the secret’s value, under the name the pipeline reads,SERVICE_ACCOUNT_JSON:
Publishing to more than one project? Store each key under its own name (
STAGING_SA,
ACME_PROD_SA) and bind it per dataset with secret_map, exactly as sources and providers do. The pipeline
still asks for SERVICE_ACCOUNT_JSON; the map says which secret answers.3. Publish
From the dataset page, Publish asks for the bucket, a folder name (the dataset’s name by default), which secret holds the key, and whether to send only the rows a human has validated. From a terminal, with the CLI:More than one bucket: dev, staging, prod
The layout below is the same in every bucket, so a second destination is setup only — no different commands. Repeat steps 1 and 2 against that project and store its key under its own secret name:What lands in the bucket
audio-pipeline workflow for objects under them), so the manifest is uploaded last: by the time
anything downstream reads a line, the audio that line names is already there. The dev and prod buckets have
no such trigger today, but the layout is deliberately identical so a dataset moves between them unchanged.
The manifest keeps the column names the structured folder already uses (FileName, SegmentID,
SegmentStart, SegmentEnd, Speaker, GroundTruthText, ProcessedText, …), so a reader written for the
datasets already in that folder reads this one too. Four columns are ours and sit at the end: Split,
Status, CallID and Provider — a reader that selects by name ignores them, and a person opening the CSV can
see which rows a human signed off.
For a speech dataset, one recording is uploaded once even though many rows are chunks of it; each row’s
SegmentStart and SegmentEnd cut it. GroundTruthText is the transcript a person confirmed, falling back to
the model’s draft when there is none — so publishing early gives drafts, and publishing again after annotation
replaces them.
If the dataset’s policy is Mask, the manifest
carries the masked text: [NAME], [PHONE] and the rest in place of what the scan found, or of what a
reviewer settled where a row was reviewed. The recordings go as
recorded, because Mask covers text and not audio. A manifest is read by a training pipeline, so a publish
doesn’t write a line it can’t vouch for: if a row’s text changed since the last scan, the run stops before it
uploads anything and asks you to scan again.
Publish isn’t the only way out. For a file to train on with made-up names and numbers in place of the real
ones, and a report that says how well the scan was checked, see
Export for training. From a terminal that’s
levers9 dataset pii export.
Interrupted uploads
Uploads are resumable, which matters at corpus scale: a dataset is thousands of files and can be tens of gigabytes. Each file goes up as a GCS resumable session, and if a transfer breaks the next attempt asks the bucket how many bytes it actually holds and continues from there rather than starting the file again. Session URIs are written beside the dataset before the first byte, so a container that is killed — evicted, restarted, cancelled — resumes mid-file. If a run fails, run it again: it picks up where it stopped.Publishing again
Publishing is safe to repeat. Audio already in the bucket is left alone (the run reports how many were skipped) and the manifest is rewritten, which is what you want after more rows are validated. Passoverwrite to send the
audio again as well. Each run appends a receipt to the dataset’s meta.json — bucket, folder, counts, and when —
so the dataset page can show where its rows went.
When it fails
The bucket’s own README
Each dataset bucket carries aREADME.md describing this layout for people who open the bucket rather than
the dashboard. The canonical copy is lab/ingest/BUCKET_LAYOUT.md in the repo — edit it there and upload,
so the buckets and the docs cannot drift:

