# Datasets_ResolvePoses

`POST /v1/accounts/{account}/datasets/{dataset}:resolvePoses`

Resolves a handheld upload's poses at each hand's TCP (poses_at TCP), or
undoes that (ROOT), by re-deriving the upload with the calibration. It
adds the `<side>.tcp.*` channels beside the gripper root's `<side>.ee.*`,
which stay exactly as they were.

The work is accepted, not finished: it is a new publication of the whole
upload, as long as the upload's video takes to check again, so the
response says only that it started. Poll it through the sync-status route
(GET /datasets/sync-status/{operation_id}) until it is finalized or
failed; GetDataset then shows the new poses_at. The old version stays
readable, and a version tag or pin cut earlier keeps the poses it had.
Resolving again with a replaced calibration is the same call; the same
calibration again, or ROOT on an upload not at the TCP, changes nothing
and answers no_op.

One re-derive of an upload runs at a time (this call, ReingestDataset and
the dashboard's automatic resolve after a robot is picked all admit one).
The same resolve asked while it runs answers the running operation, which
is the one to poll; a different one (an undo of it, another calibration)
is FAILED_PRECONDITION until it lands. There is no way to cancel one.

Only the poses at the TCP are stored. What reads them is not part of this
call: a reach check still walks the gripper root, and an export carries
`<side>.ee.*` as before.

FAILED_PRECONDITION unless the dataset is a LeRobot upload recorded on a
handheld rig whose state ingest converted (or a raw MCAP recording with
tracked poses); also on a mixture, a merged dataset (every
upload of a merge sits at one frame, so resolve them before they merge)
and an upload merged into one (naming it). The re-derive is built on the
version that was current when it started, so it fails (the status route
says so) when a review ruling or a trim lands first: resolve again.

- Base URL: `https://junction-api.qualiastudios.dev`
- Authentication: an API key in the `X-API-Key` header

## Parameters

- `account` (path, string, required): The account id.
- `dataset` (path, string, required): The dataset id.

## Request body

Required: `application/json` `qualia.v1.ResolvePosesRequest`

## Responses

- `200` OK: `application/json` `qualia.v1.ResolvePosesResponse`
- `default` Default error response: `application/json` `google.rpc.Status`

## Schemas

```yaml
qualia.v1.ResolvePosesRequest:
  required:
    - name
    - posesAt
  type: object
  properties:
    name:
      type: string
      description: accounts/{account}/datasets/{dataset}
    posesAt:
      enum:
        - POSES_AT_UNSPECIFIED
        - ROOT
        - TCP
      type: string
      description: |-
        TCP to resolve at the TCP (again, with a replaced calibration, when the
        upload is there already), ROOT to undo that. Unspecified is
        INVALID_ARGUMENT.
      format: enum
    calibration:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.TcpCalibration"
      description: |-
        The calibration to resolve with: required for TCP, empty for ROOT. The
        caller supplies it, normally the calibration of the rig the upload was
        recognised as recorded on (Dataset.poses_rig; not a dataset label, those
        are editable, and not Episode.rig, which ingest may not have used), and
        it is recorded in the upload's
        meta/ingest.toml, so the upload does not follow the rig afterwards. Both
        hands, every number finite. INVALID_ARGUMENT otherwise.
    mergedSettingsContext:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.MergedSettingsContext"
      description: |-
        Explicitly confirmed correction of one eligible LeRobot merged member.
        Its training adapter must consume observation.tcp and its own recognized
        rig supplies calibration; one rig's calibration must not be fanned out to
        heterogeneous uploads. Omitted retains the old merged-away restriction.
    requestId:
      type: string
      description: UUID retry key, required with merged_settings_context.
    validateOnly:
      type: boolean
      description: |-
        Merged datasets only: run the same per-upload admission checks without
        creating a lease or dispatching ingest. All blockers are returned by name;
        a later move rechecks them. Standalone uploads retain their existing API.
    members:
      type: array
      items:
        type: string
      description: "Merged only: retry these held uploads. Empty selects every current member."
    membershipRevision:
      type: string
      description: |-
        Required for a merged move (not validate_only), from GetDataset's
        pose_preparation. No caller calibration is accepted for merged datasets:
        the server reads each upload's own recognized rig's current calibration.
    expectedOperationId:
      type: string
      description: Resume only this existing pose operation; never admit a new move.
  description: Request for Datasets.ResolvePoses.
qualia.v1.TcpCalibration:
  required:
    - filename
    - hands
  type: object
  properties:
    filename:
      type: string
      description: The uploaded file's name, e.g. "tcp_calibration.json".
    deriveDate:
      type: string
      description: |-
        When the calibration was made, "YYYY-MM-DD": the leading date of the
        file's `calibrated` or else its `derived` text. Empty when the file gives
        none.
    hands:
      type: array
      items:
        $ref: "#/components/schemas/qualia.v1.TcpCalibration_Hand"
      description: Both hands, left then right.
    eeLink:
      type: string
      description: |-
        The link of the rig's gripper design that `Hand.ee_to_tcp` is a pose in,
        with `{side}` for "left" or "right" (the gripper joint's convention in
        RobotModel.Gripper), e.g. "openarm_{side}_ee_base_link". Read from the
        file's own description of its end-effector frame. Empty when the file
        names none, lacks the end-effector poses, or they disagree with its
        controller_to_tcp (then no `ee_to_tcp` is set either). Display only.

        `ee_to_tcp` is drawn in this link's own frame, which must be the plain
        end-effector frame the file means. An export whose link sits elsewhere
        draws the TCP off by the difference: an OpenArm exported with the xacro
        default collapse_internal_empty_links puts openarm_<side>_ee_base_link at
        link7, 20.5 mm behind the frame the calibration is in.
  description: |-
    A handheld rig's calibration of its trackers to the gripper's tool centre
    point (TCP, the midpoint of the closed fingertips). It is what a tcp_calibration.json holds as `<side>.controller_to_tcp`: the TCP's pose in
    the tracker's frame, so a pose recorded at a tracker becomes the pose of the
    TCP by composing it with this transform.

    A file that also names the gripper's end-effector link gives the same TCP in
    that link's frame (`ee_link`, `Hand.ee_to_tcp`), which is what a rig's
    gripper design (Rig.gripper) draws the TCP with. Those two are for display:
    poses are resolved and uploads converted with `tracker_to_tcp`, `filename`
    and `derive_date` alone, never with `ee_to_tcp`. So compare two calibrations
    on those, `hands[].side` and `hands[].tracker_to_tcp` only: a copy kept
    elsewhere (a dataset keeps the calibration its poses were resolved with)
    carries just those, and a whole-message comparison would call every
    calibration that has the end-effector fields replaced.
qualia.v1.TcpCalibration_Hand:
  required:
    - side
    - trackerToTcp
  type: object
  properties:
    side:
      enum:
        - SIDE_UNSPECIFIED
        - SIDE_LEFT
        - SIDE_RIGHT
      type: string
      description: Which hand.
      format: enum
    trackerToTcp:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Transform"
      description: |-
        The TCP's pose in the tracker's frame: xyz in metres and rpy in radians
        (URDF convention, extrinsic XYZ), the file's rotation matrix converted.
        A rotation whose pitch is within a hair of +-90 degrees has no unique
        roll and yaw; it is stored with roll 0.
    eeToTcp:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Transform"
      description: |-
        The same TCP in the frame of the gripper design's `ee_link` for this
        hand, in the form of `tracker_to_tcp` (so the same pitch limit). Display
        only. Set with `ee_link` or not at all, and only when it composes with
        the file's controller_to_ee to the file's controller_to_tcp, which is
        what makes the TCP drawn the TCP used.
  description: One hand's transform.
qualia.v1.Transform:
  type: object
  properties:
    xyz:
      type: array
      items:
        type: number
        format: double
      description: x, y, z in metres.
    rpy:
      type: array
      items:
        type: number
        format: double
      description: roll, pitch, yaw in radians.
  description: |-
    A 3D rigid transform: translation in metres and roll, pitch, yaw in radians
    (URDF convention, extrinsic XYZ).
qualia.v1.MergedSettingsContext:
  required:
    - mergedDataset
    - members
    - membershipRevision
    - etag
  type: object
  properties:
    mergedDataset:
      type: string
      description: The merged dataset whose membership was confirmed.
    members:
      type: array
      items:
        type: string
      description: Every upload it held, in merge order, including the selected upload.
    membershipRevision:
      type: string
      description: Opaque merge/undo epoch from GetDataset; never recapture it on retry.
    etag:
      type: string
      description: The dataset's editable etag; a member edit sends the selected upload's.
    sourceCommit:
      type: string
      description: |-
        The selected upload's exact paired head. Empty on the merged dataset's
        context, which is used only to fence global label edits.
  description: |-
    No longer answered: a merge is final and a merged dataset is not
    corrected through its uploads. One an older client still sends is checked:
    every resource in the caller's account/workspace, the exact ordered
    membership, the selected upload's etag and its published head. A stale
    context is ABORTED.
qualia.v1.ResolvePosesResponse:
  type: object
  properties:
    operationId:
      readOnly: true
      type: string
      description: |-
        The re-derive's operation id, to poll through
        GET /datasets/sync-status/{operation_id}. Empty when no_op is set.
    noOp:
      readOnly: true
      type: boolean
      description: |-
        True when nothing needed doing (the upload already stands as asked), so
        there is nothing to wait for.
    mergedPreparation:
      readOnly: true
      allOf:
        - $ref: "#/components/schemas/qualia.v1.MergedPosePreparation"
      description: |-
        Merged dataset only: one result per upload. Eligible uploads may proceed
        despite other blockers. Retry joins the same running work; no batch job
        or implicit pose move on a robot selection is created.
  description: Response for Datasets.ResolvePoses.
qualia.v1.MergedPosePreparation:
  type: object
  properties:
    membershipRevision:
      readOnly: true
      type: string
      description: |-
        Opaque ordered-membership epoch. Send it back on an explicit move or
        removal; a changed membership is ABORTED and must be confirmed again.
    currentMembers:
      readOnly: true
      type: integer
      description: Number already at their current capture calibration.
      format: int32
    totalMembers:
      readOnly: true
      type: integer
      description: Number of uploads in the current membership.
      format: int32
    members:
      readOnly: true
      type: array
      items:
        $ref: "#/components/schemas/qualia.v1.MemberPosePreparation"
      description: Preparation state for each upload, in membership order.
    applicable:
      readOnly: true
      type: boolean
      description: False when none of these recordings has handheld poses to prepare.
  description: Current TCP preparation of the uploads held by one merged dataset.
qualia.v1.MemberPosePreparation:
  type: object
  properties:
    upload:
      readOnly: true
      type: string
      description: The source upload resource.
    displayName:
      readOnly: true
      type: string
      description: The source upload's display name.
    state:
      readOnly: true
      enum:
        - STATE_UNSPECIFIED
        - CURRENT
        - READY
        - RUNNING
        - BLOCKED
        - FAILED
      type: string
      description: The upload's current preparation state.
      format: enum
    reason:
      readOnly: true
      type: string
      description: Human-readable explanation when applicable.
    operationId:
      readOnly: true
      type: string
      description: UUID polled at this upload's reingests/{operation_id} resource.
    rigKey:
      readOnly: true
      type: string
      description: Recording rig, never an editable collection tag or playback robot.
    calibrationGripper:
      readOnly: true
      type: string
      description: The capture gripper supplying the calibration.
    calibrationEtag:
      readOnly: true
      type: string
      description: The capture calibration's version.
    publishing:
      readOnly: true
      type: boolean
      description: Preparation is complete and its publication is pending; it cannot be cancelled.
  description: |-
    One upload's current state. Names and reasons are suitable for the merged
    dataset's pre-check; no source data or historical rulings are removed.
google.rpc.Status:
  oneOf:
    - $ref: "#/components/schemas/ApiError"
    - $ref: "#/components/schemas/GatewayError"
  description: "The body of a failed call: one of the two shapes below."
ApiError:
  title: API error
  type: object
  required:
    - code
    - message
    - retryable
    - details
  properties:
    code:
      type: string
      description: The gRPC code name in upper case, e.g. `NOT_FOUND`.
    message:
      type: string
    retryable:
      type: boolean
      description: Whether the same call may succeed if retried.
    details:
      type: array
      items:
        type: object
      description: "Empty, except when a dataset copy or move is refused: then one `google.rpc.ErrorInfo` with the refusal's `reason` code, its `domain` and, in `metadata`, the datasets it names."
  description: A refusal from the API itself.
GatewayError:
  title: Gateway error
  type: object
  required:
    - detail
  properties:
    detail:
      type: string
      description: What went wrong.
  description: A refusal from the gateway before the call reaches the API, for example `401` bad or missing API key, `403` key lacks the scope or environment closed, `413` body too large.
```
