annie.dataset.storage

SQLite-backed review-status store (Browse tab curation state).

This holds curation state per video — a good/bad verdict, an optional note, an “add to annotator” flag, and the protagonist-track correction — plus, on export, the protagonist _manual CSV. The store is a real file (by default a per-session ~/.annie/sessions/annie_<timestamp>.db; pin one with ANNIE_DB_PATH) so it survives restarts, and is keyed by annie.models.VideoEntry.key.

Every video is liked (good) by default: a row only exists once the user interacts, and the UI treats “no row” as good. The store is exportable to CSV/JSON in one call and importable via upsert, so a reviewer’s curation travels with them.

Attributes

Verdict

A review verdict. None (no row) is treated as "good" by the UI.

Decision

A Segment-review decision on a clip. None means the clip is not yet reviewed.

Classes

ReviewRecord

One persisted review row.

EventRecord

One persisted Event-annotation event: a labelled interval on a track.

EventCategory

One participant category (a timeline lane) in the Event-annotation task.

EventType

One event category in the Event-annotation task (e.g. "Smile").

ReviewStore

A thin, connection-per-call wrapper over the review table.

Module Contents

annie.dataset.storage.Verdict

A review verdict. None (no row) is treated as "good" by the UI.

annie.dataset.storage.Decision

A Segment-review decision on a clip. None means the clip is not yet reviewed.

class annie.dataset.storage.ReviewRecord[source]

One persisted review row.

row_key

Stable per-row identity (annie.models.VideoEntry.key).

video_id

The video this review belongs to.

annotation_suffix

The annotation suffix, or None for an exact match.

verdict

"good", "bad", or None if untouched (treated as good).

note

Free-text reviewer note (empty string when none).

annotate

Whether the video is queued for the Annotator tab.

active_track

The session’s protagonist-track override, or None if the reviewer has not corrected this video (the heuristic value then stands).

decision

The Segment-review accept/drop for a clip row, or None if the clip is not (or not a) Segment-review sample.

updated_at

ISO-8601 UTC timestamp of the last change.

class annie.dataset.storage.EventRecord[source]

One persisted Event-annotation event: a labelled interval on a track.

event_id

Stable UUID identity, so an event survives edits to its own fields.

video_id

The video this event belongs to.

row_key

The annie.core.models.VideoEntry.key of the queue row, tying the event to the same identity curation uses.

track

The track (category) name the event sits on.

start_frame

Inclusive start frame of the interval.

end_frame

Inclusive end frame of the interval (>= start_frame).

label

Free-text label shown on the event box (empty when unnamed).

note

Free-text note (empty when none).

attributes

Arbitrary string key/value pairs; stored as a JSON object.

color

Per-event colour override (hex string), or None to inherit the event category’s colour, else the track’s, on the timeline.

event_type

The event category the event was made with (and stays linked to), or None for a plain event.

updated_at

ISO-8601 UTC timestamp of the last change.

class annie.dataset.storage.EventCategory[source]

One participant category (a timeline lane) in the Event-annotation task.

DB-scoped, not per-video: the categories in a review DB are shared by every video of that dataset. An event belongs to a category when its track equals the category name.

name

The category / participant label (e.g. "Mother"); unique within the DB.

ordinal

Vertical lane order, 0 at the top.

color

Optional hex colour override for the lane; None uses the palette by ordinal.

updated_at

ISO-8601 UTC timestamp of the last change.

class annie.dataset.storage.EventType[source]

One event category in the Event-annotation task (e.g. "Smile").

DB-scoped like EventCategory. A new event made with it gets its name as the label, and stays linked: renaming the type renames its events; its colour and attributes apply wherever the event sets none of its own (see annie.dataset.events).

name

The event category’s name; unique within the DB.

ordinal

Order in the Category block (0 first; Shift+1 selects it).

color

Hex colour of its events, or None for the participant lane’s colour.

attributes

Default key/value attributes of its events.

updated_at

ISO-8601 UTC timestamp of the last change.

class annie.dataset.storage.ReviewStore(db_path: str | pathlib.Path)[source]

A thin, connection-per-call wrapper over the review table.

The store opens a fresh connection for each operation, which keeps it safe to call from the render worker threads and the UI thread alike without sharing a connection across threads.

Open (creating if needed) the review database at db_path.

Parameters:

db_path – Filesystem path to the SQLite file. Parent directories are created as needed.

has_data() → bool[source]

Return whether any review, event, participant or event category has been stored.

Used to decide whether switching to another database would leave progress behind (see copy_to()).

copy_to(db_path: str | pathlib.Path) → ReviewStore[source]

Copy this whole database to db_path and return a store opened on the copy.

Uses SQLite’s online backup API, so the copy is consistent even while this store is in use. An existing file at db_path is overwritten; this database is left untouched.

Parameters:

db_path – Destination SQLite file. Parent directories are created as needed.

Returns:

A ReviewStore backed by the copy.

get(row_key: str) → ReviewRecord | None[source]

Return the review record for row_key, or None if unreviewed.

Parameters:

row_key – The row identity to look up.

Returns:

The stored ReviewRecord, or None.

all() → list[ReviewRecord][source]

Return every review record, ordered by row key.

Returns:

All stored review records.

records_by_key() → dict[str, ReviewRecord][source]

Return every stored review record keyed by row key, in one query.

A filter pass over the whole manifest needs each video’s review state; reading them all at once replaces one connection per video with a single one.

Returns:

row_key -> ReviewRecord for every touched video (unreviewed ones are absent).

list_by_verdict(verdict: Verdict) → list[ReviewRecord][source]

Return all records with the given verdict (the good or bad list).

Parameters:

verdict – "good" or "bad".

Returns:

Matching review records ordered by row key.

upsert(row_key: str, video_id: str, annotation_suffix: str | None, *, verdict: Verdict | None = None, note: str | None = None, annotate: bool | None = None, active_track: int | None = None, decision: Decision | None = None) → ReviewRecord[source]

Insert or update a review row, preserving fields left as None.

Passing verdict=None / note=None / annotate=None / active_track=None / decision=None keeps any existing value, so the good/bad toggle, the note, the annotator flag, the protagonist correction, and the Segment-review accept/drop update independently.

Parameters:
  • row_key – Stable row identity (primary key).

  • video_id – The video id, stored for export/grouping.

  • annotation_suffix – The annotation suffix, or None.

  • verdict – New verdict, or None to leave unchanged.

  • note – New note, or None to leave unchanged.

  • annotate – New annotator flag, or None to leave unchanged.

  • active_track – New protagonist-track override, or None to leave unchanged.

  • decision – New Segment-review decision, or None to leave unchanged.

Returns:

The resulting ReviewRecord.

set_verdict(row_key: str, video_id: str, annotation_suffix: str | None, verdict: Verdict | None) → ReviewRecord[source]

Set (or clear) the good/bad verdict for a row.

Parameters:
  • row_key – Stable row identity.

  • video_id – The video id.

  • annotation_suffix – The annotation suffix, or None.

  • verdict – "good", "bad", or None to clear.

Returns:

The updated ReviewRecord.

set_annotate(row_key: str, video_id: str, annotation_suffix: str | None, value: bool) → ReviewRecord[source]

Set the “add to annotator” flag for a video.

Parameters:
  • row_key – Stable row identity.

  • video_id – The video id.

  • annotation_suffix – The annotation suffix, or None.

  • value – Whether the video is queued for the Annotator tab.

Returns:

The updated ReviewRecord.

set_annotate_many(videos: collections.abc.Iterable[tuple[str, str]], value: bool) → int[source]

Set the “add to annotator” flag for many videos in one transaction.

Backs Browse’s “Add all to Annotator” action: calling set_annotate() per video would open a connection and commit for each one, which is slow on a large filtered selection. Only the annotate column is written, so any stored verdict, note, or protagonist correction survives; videos with no row yet get one with the defaults (liked, no note).

Parameters:
  • videos – (row_key, video_id) pairs to update.

  • value – Whether the videos are queued for the Annotator tab.

Returns:

The number of videos written.

set_active_track(row_key: str, video_id: str, annotation_suffix: str | None, track_id: int) → ReviewRecord[source]

Persist the reviewer’s protagonist-track correction for a video.

The choice lives only in this session’s database; the _manual CSV is written separately by the Annotator’s “Export corrected CSV” action.

Parameters:
  • row_key – Stable row identity.

  • video_id – The video id.

  • annotation_suffix – The annotation suffix, or None.

  • track_id – The chosen active track index.

Returns:

The updated ReviewRecord.

active_tracks() → dict[str, int][source]

Return every stored protagonist override as row_key -> track_id.

Returns:

A mapping of row key to corrected track index for the rows a reviewer has changed this session (rows without an override are omitted).

annotator_keys() → set[str][source]

Return the row keys flagged for the Annotator tab.

Returns:

The set of row_key values whose annotate flag is set.

set_decision(row_key: str, video_id: str, decision: Decision) → ReviewRecord[source]

Persist a Segment-review accept/drop decision for a clip.

The clip is keyed by its composite {video_id}_{segment_id} row_key, so re-opening the source resumes a half-finished pass from the database.

Parameters:
  • row_key – The clip identity ({video_id}_{segment_id}).

  • video_id – The parent video id, stored for export/grouping.

  • decision – "accept" or "drop".

Returns:

The updated ReviewRecord.

clear_decision(row_key: str) → None[source]

Return a clip to the undecided state, dropping any accept/drop.

This is the “Undecided” escape hatch in Segment review: a misclick or a changed mind must be able to put a clip back into the undecided pool that the progress bar counts and “jump to next undecided” walks. It cannot go through upsert(), where decision=None means “leave unchanged” — this writes the NULL explicitly. A clip that was never decided is left untouched.

Parameters:

row_key – The clip identity ({video_id}_{segment_id}).

decisions() → dict[str, Decision][source]

Return every stored Segment-review decision as row_key -> decision.

Returns:

A mapping of clip key to "accept"/"drop" for the clips a reviewer has decided (undecided clips are omitted).

list_by_decision(decision: Decision) → list[ReviewRecord][source]

Return all records with the given Segment-review decision.

Parameters:

decision – "accept" or "drop".

Returns:

Matching review records ordered by row key (the accepted or dropped set).

add_event(video_id: str, row_key: str, track: str, start_frame: int, end_frame: int, *, label: str = '', note: str = '', attributes: dict[str, str] | None = None, color: str | None = None, event_type: str | None = None) → EventRecord[source]

Insert a new event on a track and return it (with its fresh id).

The event’s track is also registered in event_track if absent, so a lane created implicitly by dropping an event onto it persists like an explicit one.

Parameters:
  • video_id – The video the event belongs to.

  • row_key – The queue row identity the event ties to.

  • track – The track (category) name.

  • start_frame – Inclusive start frame.

  • end_frame – Inclusive end frame (>= start_frame).

  • label – Optional event label.

  • note – Optional event note.

  • attributes – Optional arbitrary string key/value pairs.

  • color – Optional per-event colour override (hex), or None to inherit the track.

  • event_type – The event category it is made with, or None for a plain event.

Returns:

The stored EventRecord.

update_event(event_id: str, **fields: object) → EventRecord | None[source]

Update named columns of an event, returning the updated record.

Only the columns named in fields are written (plus updated_at); any others keep their stored value. attributes is accepted as a dict and stored as JSON. Passing an unknown column raises, so a typo fails loudly rather than silently doing nothing.

Parameters:
  • event_id – The event to update.

  • **fields – Column/value pairs among track, start_frame, end_frame, label, note, attributes, color, event_type.

Returns:

The updated EventRecord, or None if no such event exists.

delete_event(event_id: str) → None[source]

Delete one event by id (a no-op if it does not exist).

Parameters:

event_id – The event to delete.

get_event(event_id: str) → EventRecord | None[source]

Return one event by id, or None if it does not exist.

Parameters:

event_id – The event to fetch.

Returns:

The stored EventRecord, or None.

events_for(row_key: str) → list[EventRecord][source]

Return a video’s events, ordered by track then start frame.

Parameters:

row_key – The queue row identity to fetch events for.

Returns:

The events on that video, grouped by track and sorted within each track.

all_events() → list[EventRecord][source]

Return every stored event, ordered by video then track then start frame.

Backs the session-wide export (every annotated video in one file).

Returns:

All events in the store.

add_track(row_key: str, track: str) → None[source]

Create an empty track (lane) for a video, appended after existing ones.

Idempotent: re-adding an existing track leaves its ordinal untouched.

Parameters:
  • row_key – The queue row identity.

  • track – The track (category) name.

rename_track(row_key: str, old: str, new: str) → None[source]

Rename a track and move its events onto the new name, keeping the ordinal.

A no-op when old and new are equal. If new already exists, the two lanes merge: old’s events move onto new and the now-empty old lane is removed.

Parameters:
  • row_key – The queue row identity.

  • old – The current track name.

  • new – The new track name.

delete_track(row_key: str, track: str) → None[source]

Delete a track and all of its events for a video.

Parameters:
  • row_key – The queue row identity.

  • track – The track (category) name to remove.

tracks_for(row_key: str) → list[str][source]

Return a video’s track names in their vertical order.

Parameters:

row_key – The queue row identity.

Returns:

The track names ordered by ordinal (their top-to-bottom lane order).

event_row_keys() → set[str][source]

Return the row keys that have at least one event or track.

Lets the tab know which queued videos already carry annotation work.

Returns:

The set of row keys present in either event table.

categories() → list[EventCategory][source]

Return the dataset’s participant categories, in lane order.

Returns:

The categories ordered by ordinal then name (top to bottom).

add_category(name: str, color: str | None = None) → EventCategory[source]

Add a category at the end of the lane order (idempotent on the name).

Re-adding an existing name is a no-op that returns the existing category, so a double-click never creates a duplicate.

Parameters:
  • name – The category / participant label (must be non-empty; caller validates).

  • color – Optional hex colour override for the lane.

Returns:

The stored EventCategory (existing one if the name was already present).

rename_category(old: str, new: str) → None[source]

Rename a category and move every event on it onto the new name.

A no-op when old == new. If new already exists, the two lanes merge: old’s events move onto new and the now-empty old category is removed. This is the safe way to fix a label everywhere at once — events follow, so nothing is orphaned.

Parameters:
  • old – The current category name.

  • new – The new name.

delete_category(name: str) → None[source]

Delete a category and all events on it, then close the ordinal gap.

The caller is responsible for confirming when the category still holds events (the UI guards this); at the store level the delete always cascades to keep the data consistent — an event can never reference a category that no longer exists.

Parameters:

name – The category to remove.

category_event_count(name: str) → int[source]

Return how many events currently sit on a category (for the delete guard).

Parameters:

name – The category name.

Returns:

The number of events whose track equals name.

reorder_categories(names: list[str]) → None[source]

Set the lane order to names (first = top). Unlisted categories keep going after.

Parameters:

names – The category names in the desired top-to-bottom order. Names not present in the DB are ignored; categories omitted from the list are appended after the listed ones in their previous relative order.

set_category_color(name: str, color: str | None) → None[source]

Set (or clear) a category’s lane colour override.

Parameters:
  • name – The category name.

  • color – A hex colour, or None to fall back to the palette by ordinal.

seed_categories_from_events() → int[source]

Seed the category table from distinct event track names, if it is empty.

Back-compat for a DB annotated under the old single-lane model: its events sit on the "events" track (or whatever names were used), but there are no category rows yet. This creates one category per distinct existing track name so those events keep a lane the user can then rename to a participant. Does nothing once any category exists.

Returns:

The number of categories created (0 if the table was already populated or there are no events).

event_types() → list[EventType][source]

Return the dataset’s event categories, in their order (Shift+1 is the first).

Returns:

The event categories ordered by ordinal then name.

add_event_type(name: str, color: str | None = None, attributes: dict[str, str] | None = None) → EventType[source]

Add an event category at the end of the order (idempotent on the name).

Parameters:
  • name – The event category’s name (must be non-empty; caller validates).

  • color – Optional hex colour of its events.

  • attributes – Optional default key/value attributes of its events.

Returns:

The stored EventType (the existing one if the name was already present).

rename_event_type(old: str, new: str) → None[source]

Rename an event category; its events follow.

Events made with it point at the new name, and those whose label is still the old name are relabelled (a label the user changed is kept). If new already exists, the two merge: old’s events move onto new and old is removed.

Parameters:
  • old – The current name.

  • new – The new name.

delete_event_type(name: str) → None[source]

Delete an event category without changing how its events look.

Each of its events first takes over what it inherited: the category’s colour (unless the event has its own) and its default attributes (the event’s own values win). Then the link is cleared, so the events stay as they were, just no longer linked.

Parameters:

name – The event category to remove.

event_type_event_count(name: str) → int[source]

Return how many events are linked to an event category (for the delete warning).

Parameters:

name – The event category’s name.

Returns:

The number of events whose event_type equals name.

reorder_event_types(names: list[str]) → None[source]

Set the event categories’ order to names (unlisted ones keep going after).

Parameters:

names – Event category names in the desired order; unknown names are ignored.

set_event_type_color(name: str, color: str | None) → None[source]

Set (or clear) an event category’s colour; its events follow unless they have their own.

Parameters:
  • name – The event category’s name.

  • color – A hex colour, or None for the participant lane’s colour.

set_event_type_attributes(name: str, attributes: dict[str, str]) → None[source]

Replace an event category’s default attributes.

Parameters:
  • name – The event category’s name.

  • attributes – The new default key/value attributes.

set_note(row_key: str, video_id: str, annotation_suffix: str | None, note: str) → ReviewRecord[source]

Set the free-text note for a row.

Parameters:
  • row_key – Stable row identity.

  • video_id – The video id.

  • annotation_suffix – The annotation suffix, or None.

  • note – The note text.

Returns:

The updated ReviewRecord.

export_json_text() → str[source]

Render all review records as a JSON array string.

export_csv_text() → str[source]

Render all review records as CSV text.

export_json(path: str | pathlib.Path) → pathlib.Path[source]

Write all review records to a JSON array file.

Parameters:

path – Destination JSON path.

Returns:

The path written.

export_csv(path: str | pathlib.Path) → pathlib.Path[source]

Write all review records to a CSV file.

Parameters:

path – Destination CSV path.

Returns:

The path written.

import_records(records: collections.abc.Iterable[dict[str, object]]) → int[source]

Upsert review records from dicts (e.g. a re-imported export).

Parameters:

records – Iterable of dicts with at least row_key and video_id.

Returns:

The number of records imported.