Episodes
client.episodes reads and edits the takes of a dataset, the way the episode
studio does. It needs your account (see
Configuration). An episode is named through the
dataset it is read from, accounts/{a}/datasets/{d}/episodes/{e}; a method on
one episode takes that full name, or its id with dataset=. Replies are the
generated qualia.v1 models.
An API key acts as the person who made it, inside that key’s organization and scopes, with the same permissions that person has in the dashboard. A take someone has open in the studio is theirs while they edit it: a single write made with a key is refused, and a bulk write skips it and reports it as locked.
from qualia import Qualia
client = Qualia()upload = "accounts/<ACCOUNT_UUID>/datasets/<DATASET_UUID>"
page = client.episodes.list(upload, filter="state = UNREVIEWED", limit=50)episode = client.episodes.get(page.items[0].name)| Method | What it reads |
|---|---|
list(dataset, filter=, order_by=, view=, version=, limit=, cursor=) | One page of episodes, in recording order |
get(episode, dataset=, view=, version=) | One episode, with its posters and markers |
locate(episode, dataset=, filter=, order_by=, limit=, version=) | Its position in a filtered list, and its neighbours |
summarize(dataset, filter=, facets=, metrics=, bin_count=) | Counts, facets and metric spreads over what a filter leaves |
sign_videos(episode, dataset=, version=) | One short-lived signed link per camera |
sample_series(episode, dataset=, max_points=, streams=, version=) | State, action or TCP channels, sampled evenly over the take |
batch_sample_series(dataset, episodes, max_points=, streams=, version=) | The same for up to 50 takes in one call |
revisions(episode, dataset=, revisions=, limit=, cursor=) | The take’s edits, newest first, then the recording as revision 0 |
version= reads as a version froze it: a tag, "stable", "v0" or a digest
prefix. Each episode then carries the fields, trim and subtasks of the revision
that version pinned; its standing, issues and markers are current. Leave it out
(or pass "head") to read the episodes as they are now.
frozen = client.episodes.get(episode.name, version="1.0")print(frozen.revision, frozen.trim)sign_videos links stop working at each video’s expire_time; call it again
for fresh ones. When a camera file holds more than the take, start_seconds and
end_seconds say where the take is in it. A take in no published version yet (a
split child before its publish) answers unpublished=True and no videos.
streams= names the vectors to sample: "STATE", "ACTION" and "TCP" (a
handheld take resolved at its tool centre points). Left out, it samples STATE
and ACTION. batch_sample_series answers one result per take, in the order
asked; a take that cannot be read fails alone, with series unset and
error_code / error_message set. The takes times max_points (400 by
default) must stay at or under 20000.
batch = client.episodes.batch_sample_series( upload, [e.name for e in page.items[:10]], max_points=200, streams=["STATE"])for result in batch.results: if result.series is None: print(result.name, result.error_message)Edit history
Section titled “Edit history”A revision holds the take’s whole content after the edit, not the change. What an edit changed is its difference from the revision before it. Rulings (keep, reject) are not revisions, and an episode nobody edited lists nothing.
history = client.episodes.revisions(episode.name, limit=20)for revision in history.items: who = revision.author.display_name if revision.author else "" print(revision.revision, revision.create_time, who, revision.note)
# exactly one edit and the one before itpair = client.episodes.revisions(episode.name, revisions=[4, 3])update(episode, ...) edits one take and writes one revision. Only the
arguments given change. update_many(dataset, edits) makes up to 500 such edits
in one call, all or none, each an EpisodeEdit or a mapping of its fields.
| Argument | Meaning |
|---|---|
task, instruction, scene, method | Text labels; "" clears one |
tags | Replaces the free tags; [] clears them |
outcome | "SUCCESS", "PARTIAL", "FAILURE" or "OUTCOME_UNSPECIFIED" |
trim / clear_trim=True | The trim window {"start_seconds", "end_seconds"}, or the full recording again |
subtasks | Subtask spans {"start_seconds", "end_seconds", "label"}; they replace the set |
etag | The etag you read; a stale one is refused (QualiaAPIError, code ABORTED) |
request_id | Idempotency key; a retry with the same one returns the first result |
client.episodes.update( episode.name, outcome="FAILURE", tags=["slow"], trim={"start_seconds": 1.5, "end_seconds": 12.0}, etag=episode.etag,)Other writers in the same account can keep an edit from publishing for longer
than the server retries it. The server then answers 409 “account inputs
changed” and writes nothing, and the SDK sends the same call again, up to 3
tries, 1 s then 2 s apart. The etag, or the revisions a request_id froze on
its first call, are checked again each time, so a change another writer made to
the same episode is still refused.
Trims and task links are made one episode per call. trim_to_markers,
clear_trims and name_subtasks do it across many episodes from their own
recorder markers (Data has examples).
split(dataset, cuts, keep=, validate_only=) cuts episodes of an upload into
child episodes. cuts maps an episode to its cut points in seconds. keep maps
an episode to the windows to keep instead, (start, end) in time order and not
overlapping: each window becomes one child, and what lies outside every window
is in no child. An episode is cut one way, not both. validate_only=True
reports the children without writing them.
plan = client.episodes.split( upload, keep={episode.name: [(0.5, 6.0), (8.0, 14.0)]}, validate_only=True)split = plan.splits[0]print(split.children, split.dropped_between_frame_count)split_at_markers cuts at recorder markers, and unsplit removes children that
were never published.
keep, reject, reset and restore take the dataset, then episode ids or
names, or one filter=. They answer a RulingResult with the changed,
unchanged, skipped and locked counts.
result = client.episodes.reject(upload, filter='open_issue_codes:"jerky"', reason="jerky")print(result.changed_count, result.locked_count)Link to a Task
Section titled “Link to a Task”link_task(episode, task, dataset=, etag=, request_id=) links one take to a
Task, or removes its own link with "". A link is not an edit: the label,
revisions and versions stay as they are. Unlinked, the take reads the Task of
the dataset that holds it again (linked_task_inherited). assign_task labels
and links many takes at once, and link_task_labels links the takes whose label
names a Task.
linked = client.episodes.link_task(episode.name, "<TASK_UUID>", etag=episode.etag)client.episodes.link_task(linked.name, "") # back to the dataset's Task