artful

artful — Storyboard data model and exporters.

A storyboard is a sequence of panels along a timeline. Each panel pins an interval of the master asset (a song, video, podcast clip), optionally points at a project shot, and carries one or more images plus directorial annotations.

Panels are persisted as lacing.Annotation records with body schema annot://schema/storyboard-panel/v1. That means a storyboard is queryable with lacing’s full toolkit (Allen interval algebra, store backends, format adapters) without artful having to reinvent any of it.

Public surface:

class artful.ModelSheet(*, character_ref_id: str, sheet_id: str, canonical_views: dict[~typing.Literal['front', 'three_quarter_front', 'side_left', 'side_right', 'three_quarter_back', 'back'], str] = <factory>, expression_set: dict[str, str] = <factory>, costume_set: dict[str, str] = <factory>, palette_anchors: tuple[str, ...] = <factory>, distinguishing_features: tuple[str, ...] = <factory>, head_to_body_ratio: ~typing.Annotated[float | None, ~annotated_types.Gt(gt=0.0)] = None, do_not_do: tuple[str, ...] = <factory>, age_progression: dict[str, str] = <factory>, prop_interactions: dict[str, str] = <factory>, voice_personality_notes: str | None = None)[source]

The body of a model-sheet annotation — a character’s canonical turnaround, expression set, and supporting metadata.

Produced by character_to_modelsheet.<flavor>.<model> (spec §7.4). Lives in artful (next to PanelBody) because it is a storyboard-adjacent asset: model sheets feed into per-panel renders as reference images, and the inspector surfaces them alongside the panels they conditioned.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class artful.PanelBody(*, panel_id: str, shot_id: str | None = None, images: tuple[~artful.schema.PanelImage, ...]=<factory>, caption: str = '', framing: str = '', camera: str = '', transition_in: str = 'cut', notes: str = '', review_status: Literal['unreviewed', 'approved', 'needs-revision', 'rejected']='unreviewed', active_image_index: Annotated[int, ~annotated_types.Ge(ge=0)] = 0, moment_caption: str | None = None, moment_heuristic: Literal['decisive_composition', 'apex_of_action', 'just_before_recognition', 'held_breath', 'reaction_not_action', 'entry_for_clip', 'expression_over_action', 'silhouette_test', 'eyeline_contact'] | None=None, moment_timing: Literal['entry', 'mid', 'apex', 'exit'] | None=None, moment_focal_character_ref: str | None = None, split_from_beat_id: str | None = None, split_index: int | None = None, split_total: int | None = None, split_reason: str | None = None, shot_size: Literal['ECU', 'CU', 'MCU', 'MS', 'MWS', 'WS', 'LS', 'ELS', 'INSERT', 'TWO_SHOT', 'THREE_SHOT', 'GROUP', 'MASTER'] | None=None, angle: Literal['EYE_LEVEL', 'HIGH', 'LOW', 'DUTCH', 'BIRDS_EYE', 'WORMS_EYE', 'PROFILE', 'THREE_QUARTER', 'OTS', 'POV'] | None=None, movement: Literal['LOCKED', 'PAN', 'TILT', 'DOLLY_IN', 'DOLLY_OUT', 'TRUCK', 'PEDESTAL', 'ZOOM', 'CRANE', 'HANDHELD', 'WHIP_PAN', 'DOLLY_ZOOM'] | None=None, duration_seconds_estimate: Annotated[float | None, ~annotated_types.Ge(ge=0.0)] = None, duration_source: Literal['estimate', 'from_voiceover', 'from_clip', 'manual'] | None=None, duration_confidence: Annotated[float | None, ~annotated_types.Ge(ge=0.0), ~annotated_types.Le(le=1.0)] = None, duration_model_version: str | None = None)[source]

The body of a storyboard-panel annotation.

Persisted as the body dict of a lacing.Annotation whose body_schema_uri is PANEL_BODY_SCHEMA_URI.

The annotation’s reference (a lacing.MediaRef) carries the interval — i.e. the time span this panel covers — so we don’t duplicate interval information here.

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class artful.PanelImage(*, artifact_id: str | None = None, url: str | None = None, path: str | None = None, role: str = 'thumbnail', caption: str = '')[source]

One image associated with a panel.

Either an artifact_id (pointing into lacing’s Artifact registry by content hash) or a direct url / path is required. role distinguishes “this is the thumbnail you show on the contact sheet” from “this is the seed image for the renderer.”

model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class artful.Storyboard(*, title: str = '', asset_id: str, panels: tuple[~artful.schema.PanelBody, ...]=<factory>, style: str = '', aspect: str = '16:9')[source]

A typed view over a sequence of panel annotations.

Storyboards are constructed from in-memory data and persisted by writing each panel as a lacing Annotation. Use the helpers in artful.store to round-trip with a lacing store.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class artful.StoryboardMetaBody(*, title: str = '', style: str = '', aspect: str = '16:9')[source]
model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

artful.from_markdown(text: str) tuple[Storyboard, dict[str, TimeInterval]][source]

Parse to_markdown()’s output back into a Storyboard + intervals.

Lines that don’t match a known shape are appended to the current panel’s caption. Panels without an interval in the heading are returned with no entry in the intervals dict (caller can pin them later).

artful.load_storyboard(store: IntervalAnnotationStore, *, asset_id: str, tier: str = 'storyboard') Storyboard[source]

Read panels from store and reconstruct a Storyboard.

Filters by tier (default "storyboard") and asset_id (so a store holding multiple storyboards over multiple assets stays clean). Panels are returned ordered by interval start.

artful.new_panel_id(prefix: str = 'p') str[source]

Generate a fresh short panel id (e.g. "p3f7c1").

artful.panel_intervals_from_panels(panels: Iterable[tuple[str, float, float]]) dict[str, TimeInterval][source]

Convenience: build the panel_intervals dict for save_storyboard().

Takes an iterable of (panel_id, start_s, end_s) triples. Returns a dict keyed by panel_id with TimeInterval values.

artful.save_storyboard(storyboard: Storyboard, store: IntervalAnnotationStore, *, panel_intervals: dict[str, TimeInterval], tier: str = 'storyboard', was_attributed_to: str = 'user:unknown', was_generated_by: str = 'agent:artful') list[Annotation][source]

Persist storyboard into store.

Parameters:
  • storyboard – The Storyboard to persist.

  • store – A lacing store (any IntervalAnnotationStore).

  • panel_intervals – Per-panel-id mapping to a lacing.TimeInterval. Required because PanelBody itself does not carry the interval (it lives on the annotation’s reference); the caller must tell us which time span each panel covers.

  • tier – Tier name for the persisted annotations. Defaults to "storyboard" so multiple storyboards over the same asset can be distinguished by tier.

  • was_generated_by (was_attributed_to /) – Provenance fields applied to every persisted annotation.

Returns:

The list of persisted Annotation instances (panels, then meta).

artful.to_html(storyboard: Storyboard, panel_intervals: dict[str, TimeInterval] | None = None) str[source]

Render a self-contained HTML contact sheet.

artful.to_markdown(storyboard: Storyboard, panel_intervals: dict[str, TimeInterval] | None = None) str[source]

Render storyboard as Markdown.

If panel_intervals is given, each panel’s heading includes its [start..end]s interval. If not, only ids are shown — useful when the intervals haven’t been pinned yet.