# Curation

> Keep and reject episodes, trim and split them, name subtasks, run checks, mix datasets and save versions

Curation decides which episodes of a dataset train. You do it in the
dashboard, on the dataset page and in the episode studio. The CLI and the SDK
can do the same edits in bulk.

Every episode stands one of three ways:

- **kept**: nothing that blocks is open on it. It trains. A kept episode that
  nobody has ruled on is **kept automatically**: the rail and the studio mark it
  with an outline instead of the filled check, and keeping it yourself makes it
  yours. It does not mean no issue was ever found: an issue someone dismissed
  also leaves an episode kept.
- **flagged**: an issue that blocks a keep is open on it. It waits for a
  person to keep it, reject it, or dismiss the issue.
- **rejected**: left out of training.

The API, the CLI and the SDK call flagged `UNREVIEWED` (a filter says
`state = UNREVIEWED`), and the dashboard's own filter text keeps
`state:unreviewed`.

In the rail, a row a person kept is tinted green, and one a person rejected
red, with a bar of its colour down the row's edge. A row kept automatically, one
rejected by ingest validation or by a rule, and a person's keep flagged again
since stay plain.

Nothing here deletes data. A reject leaves the episode out of training; the
recording stays and the reject can be withdrawn. An issue is a flag on an
episode, never a removal. An outlier on the **outliers** tab raises no issue
and changes no ruling: an outlier that was kept is still kept.

## Keep and reject in the studio

Open a dataset from **Datasets**. The rail on the left lists every episode, under
the **all** tab. Its other tabs, **flagged**, **kept** and **rejected**, each
say how many episodes they hold; pick one to list only those. Click an episode
in the rail to open it in the studio, or click **review** (or press Space) to
walk the flagged episodes one by one. Review moves the whole page to the
**flagged** tab, so its chip, the counts and the trajectories follow; click
the chip, or **all**, to bring every episode back. An episode an issue flags
keeps its yellow issue count in every tab.

The **decision** panel has two buttons:

| Button   | Key       | What it does                           |
| -------- | --------- | -------------------------------------- |
| `keep`   | Enter     | Keep the episode; it trains            |
| `reject` | Backspace | Reject the episode; it leaves training |

A ruling moves you to the next episode. Other keys in the studio:

| Key    | What it does                             |
| ------ | ---------------------------------------- |
| ↑ / ↓  | Previous / next episode under the filter |
| Ctrl+Z | Withdraw your last ruling                |
| f      | Go to the box for a note or an issue     |
| c      | Cut the take at the playhead             |
| 1 – 4  | The rail: all, flagged, kept, rejected   |
| Space  | Play / pause                             |
| Esc    | Back to the dataset                      |
| ?      | Show every key                           |

Hold ↑ or ↓ and the rail moves with the key. The episode opens where you stop,
and only that one counts as viewed.

Keep, by Enter or the **keep** button, closes the issues open on the episode in
the same step, with no question asked. A toast says how many it closed and has
an **Undo** button. Reject leaves the issues open: they stay as the record of
why.

Undo, and Ctrl+Z, withdraw the ruling; they do not restore an earlier one (a
keep made over a reject does not bring the reject back). Ctrl+Z withdraws the
ruling you just made even after the studio has moved on to the next episode,
and takes you back to it; on any other episode you opened since, it withdraws
that ruling all the same and names the episode, and you stay where you are. A withdrawn keep opens again the issues it closed, and
the episode then stands as derived: flagged while a blocking issue is open on
it, kept otherwise.

## Keep and reject in bulk

The dataset page rules on whatever the current filter leaves. There are no
checkboxes. Narrow the page first:

- the rail tabs: all, flagged, kept, rejected
- the filter chips and presets, for example "outcome: failure", "length < 20s",
  "ruled by a person" (a person kept or rejected it) or "kept automatically"
- a segment of the standing bar, or an issue kind in the **issues** table

The count beside the filter says how many episodes are left. Then click
**Keep** or **Reject** (or press Enter or Backspace). A dialog asks first:
"Keep _n_ episodes?". At most 10,000 episodes are ruled on at once; narrow the
filter if there are more.

From the CLI, the same filter selects the episodes. Count and list what it
leaves first, then rule on it, or on episodes by id:

```bash
qualia dataset summary <DATASET_UUID> --filter 'state = UNREVIEWED' --facet open_issue_codes
qualia dataset episodes <DATASET_UUID> --filter 'state = UNREVIEWED AND open_issue_codes:"jerky"'
qualia dataset keep <DATASET_UUID> --filter 'state = UNREVIEWED AND open_issue_codes:"jerky"'
qualia dataset reject <DATASET_UUID> <EPISODE_UUID> --reason "operator in frame"
qualia dataset reset <DATASET_UUID> <EPISODE_UUID>
qualia dataset restore <DATASET_UUID> <EPISODE_UUID> --reason "the fps metadata was right"
```

`open_issue_codes` and `open_issue_count` see blocking issues only. Check
findings that do not block an episode (recording and graph check findings, and
VLM verdicts below their confidence bar) are notes: filter on `note_count`,
e.g. `--filter 'note_count > 0'`.

Each ruling prints how many episodes changed, already stood that way, or were
skipped. `reset` withdraws a ruling, as Ctrl+Z does. `restore` puts back an
episode ingest validation rejected. A take someone has open in the studio is
left as it was: the command lists it with who holds it on stderr, and you run it
again once they are done. See
[`qualia dataset keep`](https://junction-docs.qualiastudios.dev/cli/commands.md#qualia-dataset-keep).

## Trim, split and subtasks

A **trim** keeps one continuous stretch of a recording. A **split** keeps two
or more stretches. A **subtask** labels a span, such as “grasp” or “place”.
All three are saved as a draft on the original take; they do not change the
dataset or an existing version while you work.

Open the studio's **editor** tab:

- Drag a trim handle, or select a stretch in the film and keep or drop it.
- Click **cut** (or press **c**) at the playhead; drag the cut to move it.
  Drop the unwanted stretches between cuts.
- Handles and cuts snap to recorder markers. Hold **Ctrl** (**⌘** on Mac)
  to place one freely, or switch snapping off.
- In **subtasks**, name a track and add or move its segments. Segments
  cannot overlap; the pieces you publish keep the subtasks inside them.
- **reset** discards the saved drawing, with a short chance to take it back.

The studio saves as you draw. A saved draft stays when you walk, rule on the
take, leave or return later. A failed or unsaved drawing asks whether to stay
or discard it before leaving. Another person editing the take makes it view
only; after they leave, an authorized teammate can continue the shared draft.
Nothing applies a drawing to the live recording. Labels and keep/reject
rulings are separate changes.

An issue with timed evidence can open the editor at that stretch or suggest
a dropped stretch. That drawing is saved like any other draft. It does not
dismiss or reopen the issue; manage the issue or ruling separately.

Publish turns the selected saved drawings into a version. Each kept window
becomes an immutable episode of that version: take 12 split in two reads as
12.1 and 12.2, with its source window and original take still linked. The
original take and its draft stay available for another edit. A published
piece is read only; change the original draft and publish another version.

The CLI and SDK draw the same drafts. Marker helpers can cover a selection:

```bash
qualia dataset markers <EPISODE_UUID> -d <DATASET_UUID>
qualia dataset trim <EPISODE_UUID> -d <DATASET_UUID> --start-at-first ready --end-at-last done
qualia dataset subtasks <EPISODE_UUID> -d <DATASET_UUID> --at grasp --at place
```

See [`qualia dataset trim`](https://junction-docs.qualiastudios.dev/cli/commands.md#qualia-dataset-trim),
[`qualia dataset subtasks`](https://junction-docs.qualiastudios.dev/cli/commands.md#qualia-dataset-subtasks) and
[`qualia dataset split`](https://junction-docs.qualiastudios.dev/cli/commands.md#qualia-dataset-split).

## Issues

An issue says something is wrong with an episode. A check raises it, or the
operator while recording, or a person. A note is the same kind of message that
asks nothing of anyone.

An open issue that blocks holds the keep: the episode stays flagged until
someone keeps it, rejects it, or dismisses the issue. An issue never deletes or
rejects an episode.

Each check holds an episode by itself. A pass from the VLM check closes only
that check's own issues, so a rule can still hold an episode the model passed.
Which of the two is right is your call: keep the episode, which closes its open
issues, or dismiss the issue.

A rule states its limit in the issue:
`action repeated one sample for 7.2 s (limit 5 s)`. The limits of the rules an
upload runs are fixed;
[data collection](https://junction-docs.qualiastudios.dev/global/guides/data-collection.md#ingest-degrades-it-does-not-refuse)
lists them. A heuristic check takes each limit from a bound confirmed for the
robot, else from the range measured on the robot's own episodes, else, for
duration, from the Task, else from its own default, and its finding says which
(`high = 0.000127 (fence)`). Where a fixed limit does not fit a task, as when
holding still is part of it, dismiss that kind of issue on the dataset.

A merged dataset, or a mixture, is measured against the robot interface its
uploads share, and against their robot when they share one, as an upload is. A
mixture over several robots is measured against every robot on that interface,
and only limits confirmed for every robot apply.
When its uploads do not share one interface (a merged dataset's are still
settling to one) there is nothing to measure against: jerk, noise and velocity
are reported unassessed, and the other checks use the Task and their own
defaults.

On the dataset page, the **issues** table lists issues by kind, with who raised
them and what they say. It has two views, **open** and **dismissed**.

- Click a kind to narrow the page to its episodes. Then open an episode from
  the rail.
- **×** dismisses that kind on every episode of the dataset. Episodes with no
  other open issue become kept. **reopen** on the dismissed view undoes it.

In the studio, the notes and issues of the open episode are on the **Notes &
issues** tab. Click one to open its thread. A thread can be dismissed
(**dismiss**) or reopened (**reopen**), or replied to. Press **f** to go to the
box under them. **Enter** is a new line there: the **Add note** and **Raise
issue** buttons (and **Reply** in a thread) are the only way to send, because
Enter keeps the episode in the studio. An issue puts the episode back in review
until someone rules on it.

## Checks and the VLM check

The **Checks** panel on the **overview** tab lists the checks that can run on
the dataset, by category, with each one's last result. Run them with **Run
all**, **Run category** or **Run selected**. The **Run checks** dialog lets you
pick the episodes (all, or the current filter), check only a sample, and set a
**Credit ceiling**. It shows the estimate first: "Up to _c_ credits for _e_
episodes and _k_ checks." Findings land in the **issues** table.

The **VLM check** has a vision-language model read each episode against the
published Task it was recorded for. It costs credits. It reads only episodes
linked to a published Task and with a published video; see
[Robots and tasks](https://junction-docs.qualiastudios.dev/global/guides/robots-and-tasks.md) for linking a Task.

- A confident failure (80% or more) becomes an issue. It holds the keep: the
  episode leaves kept, and mixtures stop taking it, until someone keeps or
  rejects it. The automatic check after an upload does the same, unless it read
  the episode against a Task that is not published yet.
- A doubtful failure or an uncertain reading becomes a note.
- Once you keep an episode, what a later run finds on it is a note.
- A new run refreshes what the last one found. Your dismissals stay.

Where the check catalog is not available, the overview shows a **VLM check**
panel instead. Its **run…** button opens **Run the VLM check**, with the same
choices and the credits it will reserve.

From the CLI, estimate first; it starts and charges nothing:

```bash
qualia check estimate <DATASET_UUID> --max-episodes 50
qualia check run <DATASET_UUID> --max-episodes 50 --max-cost 200 --wait
qualia check findings <DATASET_UUID>
```

`--max-episodes` checks a sample; `--max-cost` caps the credits. `qualia
dataset checks list|run` runs catalog checks by id or category (`--kind
heuristic` for the free heuristic checks), and `qualia dataset checks findings`
reads what they found. See
[`qualia check`](https://junction-docs.qualiastudios.dev/cli/commands.md#qualia-check).

## Reach solver

The reach solver walks each handheld take on a robot and finds where the robot
cannot follow the hand. It is for UMI datasets (tagged `umi` or `egocentric`).
It needs the dataset's robot, picked in the **Setup** strip of the
**trajectories** tab, and that robot needs a model (its URDF). See
[Robots and tasks](https://junction-docs.qualiastudios.dev/global/guides/robots-and-tasks.md).

Open the **trajectories** tab. The **Setup** strip has **Play on rig**, the
**Poses** and a **Solver**, each a pick or one line; click **(?)** beside a row
for what it means. Pick the rig. If every arm of that rig has a TCP, the
poses are moved to tool center point by themselves (once, and only when they are not
there yet), and **Run solver** waits for them. Then check the **IK solver** line
(it is the rig's, shown but not a choice: you set it on the rig's Solver page,
and a rig with no solver cannot be run) and click **Run solver**. Nothing is solved until you do. When it cannot be
pressed, the reason is written beside it. A dataset that is not UMI has no
Solver row and no **Run solver** button.
When it is done, the stretches out of reach are drawn amber on the take lines.

The solver flags; it never rejects. A take is raised as an issue when the robot
cannot follow the hand for long enough in a row, or when two of the robot's
parts overlap. You decide what to keep.

To move and turn where the takes sit relative to the robot, open the
**Placement** line under the solver in the **Setup** strip: forward, left and up in
metres from the midpoint of the robot's home hands, and a heading in degrees.
The takes move on the 3D canvas as you type. Click **Save placement** to keep
the position and heading together, or **Discard** to go back to the saved ones.
**Run solver** saves a placement you have typed and not saved, then walks the takes
from it.

## A dataset's options

On **Datasets**, each card has **…** in its top right corner. Right-click a
card to open the same menu. In the list view, the menu is at the end of each
row. It offers:

- **Download**: the newest version. See [Download a version](https://junction-docs.qualiastudios.dev/global/guides/download.md).
- **Open in new tab**, **Copy link** and **Copy ID**. The ID is what the SDK
  and the CLI take, for example `qualia dataset delete <ID>`.
- **Copy to…** and **Move to…**: put it in another workspace. The dialog lists
  every workspace you can reach, grouped by organization. One you can't use
  stays in the list, greyed, with the reason under it. A copy is a new dataset
  that starts at the version you pick (the current one by default) and shares
  the content, so inside one organization nothing is uploaded again. A move
  keeps its name, history and links, and works only within its organization;
  across organizations, copy it. Adding to a workspace needs Contributor or
  higher there; moving out of a workspace, or copying out of the organization,
  needs Admin or Owner. When it is done, the dialog says what it copied, for
  example "Copied 4.0 GiB, 34 episodes in 0.8 s", and links to it. Closing the
  dialog does not stop it; a message with the same line and link says when it
  is done. Both items are also
  in **…** beside the name on the dataset's own page, where closing the dialog
  after a move takes you to the dataset in its new workspace. A version's page
  has **Copy to…** with that version picked.
- **Rename…**: changes the name it is shown by. Its address stays, so links
  keep working.
- **New mixture from this…**: a new mixture that reads it. See
  [Mixtures](#mixtures).
- **Merge with…** on an upload, or **Add uploads…** on a merged dataset: opens
  the merge with it picked. A whole merge cannot be undone, but one upload can
  later be removed and becomes a standalone upload again. The item is disabled while an
  upload cannot be merged yet; hover it to see why. If a mixture reads the
  upload as it changes (`stable` or `head`), the merge dialog says so and
  offers two ways on: keep that mixture on the upload's newest version, when
  it reads just what that version holds (it reads the same episodes after the
  merge), or remove the upload from it. If the upload changed after the list
  loaded (a new version, or new changes), keeping the mixture is refused,
  since it would drop those changes, and only the removal is offered.
- **Delete…**: the dataset leaves every list and every mixture. Nothing is
  erased, and versions already made from it keep working. A mixture that reads
  it blocks the delete until that source is removed.

Viewers see the download, open and copy items, and **Copy to…** and
**Move to…**, whose dialog says where they can copy to.

To merge several uploads at once, Shift-click each of their cards (in the
table, Cmd- or Ctrl-click the rows): a click picks only the one you click. Then
click **Merge** in the bar at the bottom of the page. Click a picked one again
to drop it, and click empty space, press Escape or click **Clear** to drop
them all. Changing a filter or collapsing a group drops the ones it hides, so
nothing hidden is merged.

## Mixtures

A mixture is a dataset that reads other datasets: uploads, other mixtures,
every upload with a tag, or every episode linked to a Task. An episode with no
Task of its own has its upload's, so a Task source reads only that Task's
episodes, even from an upload that holds several. It stays up to date as they
change.

On **Datasets**, click **mix**. In **New mixture**, give it a **Name**, choose
sources from the **tags**, **tasks**, **uploads** and **mixtures** tabs, and
click **create mixture**. Each source follows its latest version (`stable`) or
what it holds now (`head`).

To start a mixture from one dataset, click **…** on its card, then **New
mixture from this…**. **New mixture** opens with that dataset picked, named
after it and pinned to its newest version (a dataset with no version yet is
read at `head`). The mixture has its own keep and reject and its own versions;
the table below says what stays shared. Nothing is copied in storage.

A dataset source can also stay on its newest version: choose **pinned** and
that tag instead of `stable` or `head`. A mixture that follows an upload
(`stable` or `head`) keeps it from being merged; one pinned to a version does
not.

A mixture runs checks of its own each time its sources change, and raises what
they find as issues in the mixture. Two of them surprise people:

- `head_follow` ("Read at head") is raised on the mixture for every source that
  follows `head`: "… is read at head: what a run reads moves with every ruling,
  and no version can say what it read." It reads `blocks: true` but names no
  episode, so it holds none and stops no tag. Follow `stable` or pin a tag for
  a mixture you train on, as
  `client.mixtures.update_source(source, follow="stable")` does, or dismiss the
  issue.
- `mix_twin` ("Mixture: twin") is raised on two episodes from different
  uploads that are each other's closest match, and unusually close: "the same
  demonstration may have come in through two sources." It flags both in the
  mixture until you keep, reject or dismiss. It compares length, speed, jerk,
  pauses and end pose, not the pictures, so look at both before you drop one.

What belongs to a mixture and what is shared:

| Per mixture                                  | Shared when editing the original take  |
| -------------------------------------------- | -------------------------------------- |
| Keep and reject                              | Saved trim, split and subtask drawings |
| Issues raised in the mixture, and dismissals | Tags, task link and outcome            |

A reject made in a mixture belongs to that mixture. A reject on the upload
holds in every mixture; only the upload can take it back. A bulk keep or
reject in a mixture skips episodes settled upstream. Upload issues show in the
mixture as "upstream" and are settled on the upload.

One upload can feed several mixtures with different selections. Its original
take has one shared drawing; publishing freezes the selected drawing in that
version's immutable pieces. A mixture reads those edits through the published
version, according to what its source follows:

- `head`: the current recordings, without saved drawings. Saving a trim,
  split or subtask drawing does not change this source's episodes.
- `stable`: the edited pieces after the source publishes a version containing
  that drawing.
- a pinned tag: after you move the pin to a newer tag. A tag keeps the episode
  as it was when the tag was made.

## Versions

A version is a named, immutable selection for training and downloads.
Later drawings, labels, rulings or re-ingest do not rewrite it.

Click **Publish** on an upload or merged dataset. In **Publish version**:

1. Choose which groups to include. **Kept by a person** is the default;
   you may explicitly include all kept, flagged or rejected takes. You do
   not have to finish reviewing the dataset first.
2. Click **Apply all**. It saves open studios' work, freezes the selection
   and measures its exact kept windows. It changes no recording or draft.
3. Inspect the episodes, duration, edited takes, splits, measurements and
   **Takes left out**, each named with its reason. No measurement means an
   included window is unmeasured; a listed omitted take is left out.
4. Name the version, add a note and click **Create version**. It publishes
   exactly that preview as base takes and immutable cut episodes.

If you change the include groups or want newer drawings, use **Apply all
again**. A message saying takes changed since Apply all means Create still
uses the preview you inspected. A recording/settings change can require a
new preview. If creating finds another take it cannot include, it returns
for another explicit decision; inspect the updated omissions before trying
Create again. You can close the dialog while measurement or publication
continues. **Retry opening version** retries only the final read if creation
already succeeded.

The **versions** tab shows the frozen result. **Edits** lists every included
edited window and opens the exact pinned piece; **All episodes** has its own
paging and source filter. On a frozen version, search `12` to find the
original take and its `12.*` pieces, or `12.1` for its one-based piece. Search
reads the complete pinned membership and reports an error instead of partial
matches if that read is incomplete or too large. The publication's
edited/trimmed/split counts count takes; its piece count counts the new
episodes those drawings produced. The curation record below describes what
happened after that version was created.

Comparisons show before/after pieces grouped by original take, after reading
all pages. Added/removed/changed episode totals are separate from affected
takes. Changes since the newest preview-made version count changed takes,
not saves or clicks: saving an unchanged drawing adds no change.

A mixture follows or pins versions to include their published edits. A
source followed at head is **as recorded, without edits**. A mixture's own
Publish tags its resolved head; it has no draft preview. A source following
`stable` moves to the new tag; a source pinned to an older tag stays there.

Version names are 1–64 letters, digits, dots, dashes or underscores, starting
with a letter or digit. `stable`, `head` and all-hex names are reserved.

From the CLI:

```bash
qualia dataset version-preview <DATASET_UUID> --json
qualia dataset pin <DATASET_UUID> 1.0 --preview <PREVIEW_NAME> --note "first cut"
# Explicitly include another group, or tag current head without drawings:
qualia dataset pin <DATASET_UUID> 1.1 --include kept_by_person --include flagged
qualia dataset pin <DATASET_UUID> 1.2 --as-recorded
```

`version-preview` reads every page and prints windows, omissions and measured
metric count/mean/p5/p50/p95. `--json` keeps the full typed items and analysis.
Omitted/nonfinite/unmeasured or incompatible-profile values do not enter the
summaries; zero is a valid scalar. Percentiles choose sorted index floor(p×N),
capped at N−1. No pose-vector MAD or head metric is invented for a window.

`pin DATASET TAG` also creates and measures a preview when no explicit mode
is given. It refuses omitted takes unless you review and acknowledge them
with `--allow-left-out`. `--include`, `--preview`, `--as-recorded` and `--digest`
are mutually exclusive. Without TAG, `pin DEFINITION_UUID` keeps the separate
definition-pin behavior.

The SDK offers the same explicit sequence:

```python
preview = client.versions.preview(dataset)
page = client.versions.preview_items(preview)
items = list(page.items)
while page.page_info.has_next:
    page = client.versions.preview_items(preview, cursor=page.page_info.end_cursor)
    items.extend(page.items)
# Review preview.unprocessable and items before publishing this exact selection.
version = client.versions.create(dataset, "1.0", preview=preview)
```

`create` returns the actual Version. It refuses omissions unless
`allow_left_out=True`; `as_recorded=True` tags head without saved drawings.
For asynchronous control, call `preview(wait=False)` and
`publish_preview(wait=False)` yourself; both return VersionPreview. Reuse
request IDs and the same arguments when retrying a logical operation.
Timeouts do not authorize a different selection or make a replacement preview.
These helpers require the matching version-publication server and SDK/CLI
builds; the documentation does not announce a new released package version.

Re-ingest never rewrites a saved version. With the separately activated
footage-based re-cut path, unchanged recordings keep their IDs and drafts;
changed recordings get new IDs, and source settings lists state not carried
from the old takes. Legacy applied trims/splits return as recorded with their
losses named. An unavailable report is not a clean result. The production
feature flag is currently off; see [re-cut availability and results](https://junction-docs.qualiastudios.dev/global/guides/data-collection.md#re-cutting-an-upload-into-takes).

To download a version, see [Download](https://junction-docs.qualiastudios.dev/global/guides/download.md). Its windows,
task labels and normalization come from that exact pinned version, including
its immutable cut pieces; later draft saves and re-ingest do not change the
download. A LeRobot or MCAP download holds no subtasks, tags, rulings or check
findings: see
[What a LeRobot download holds](https://junction-docs.qualiastudios.dev/global/guides/download.md#what-a-lerobot-download-holds).
