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:
Storyboard,PanelBody,PanelImage— Pydantic models for in-memory work.save_storyboard()/load_storyboard()— round-trip with anylacing.IntervalAnnotationStore.to_markdown()/from_markdown()— round-trip Markdown (the canonical format for LLM authoring).to_html()— self-contained HTML contact sheet for review.
- 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
bodydict of alacing.Annotationwhosebody_schema_uriisPANEL_BODY_SCHEMA_URI.The annotation’s
reference(alacing.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 directurl/pathis required.roledistinguishes “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 inartful.storeto 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
storeand reconstruct aStoryboard.Filters by
tier(default"storyboard") andasset_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
storyboardintostore.- Parameters:
storyboard – The
Storyboardto persist.store – A lacing store (any
IntervalAnnotationStore).panel_intervals – Per-panel-id mapping to a
lacing.TimeInterval. Required becausePanelBodyitself 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
Annotationinstances (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
storyboardas Markdown.If
panel_intervalsis given, each panel’s heading includes its[start..end]sinterval. If not, only ids are shown — useful when the intervals haven’t been pinned yet.