Skip to content

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)
MethodWhat 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)

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 it
pair = 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.

ArgumentMeaning
task, instruction, scene, methodText labels; "" clears one
tagsReplaces the free tags; [] clears them
outcome"SUCCESS", "PARTIAL", "FAILURE" or "OUTCOME_UNSPECIFIED"
trim / clear_trim=TrueThe trim window {"start_seconds", "end_seconds"}, or the full recording again
subtasksSubtask spans {"start_seconds", "end_seconds", "label"}; they replace the set
etagThe etag you read; a stale one is refused (QualiaAPIError, code ABORTED)
request_idIdempotency 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_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