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.

Alongside the storyboard, artful owns two storyboard-adjacent bodies: ModelSheet (a character’s canonical turnaround) and ShotScheduleBody (the ordered, model-constraint-aware shot list that precedes the panels — see artful.shot_schedule).

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.RiskFlag(*, code: Literal['over_clip_cap', 'multi_character', 'first_last_frame', 'dialogue_plus_action', 'over_take_budget', 'unsupported_resolution', 'mixed_aspect_ratios'], message: str = '', severity: Literal['info', 'warn'] = 'warn', gotcha_id: str | None = None)[source]

One advisory warning attached to a shot (or to the whole schedule).

A cached verdict, not a constraint: it records that this shot’s requirements collided with the limits of the model named by ShotScheduleBody.advised_for_model_id. Re-advise after changing the model — ShotScheduleBody.needs_advice says when.

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

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

class artful.ShotEntry(*, shot_id: str, panel_id: str | None = None, beat_id: str | None = None, description: str = '', characters: tuple[str, ...]=<factory>, 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, max_duration_seconds: Annotated[float | None, ~annotated_types.Gt(gt=0.0)] = None, max_characters_in_frame: Annotated[int | None, ~annotated_types.Ge(ge=1)] = None, aspect: str | None = None, resolution: str | None = None, allow_last_frame_anchor: bool = False, has_dialogue: bool = False, has_action: bool = False, take_budget: Annotated[int | None, ~annotated_types.Ge(ge=1)] = None, clump_id: str | None = None, risk_flags: tuple[~artful.shot_schedule.RiskFlag, ...]=<factory>, notes: str = '')[source]

One row of a shot schedule: what to shoot, and what bounds it.

Position in ShotScheduleBody.shots is the shot’s order — there is deliberately no order field to disagree with it.

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

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

class artful.ShotScheduleBody(*, schedule_id: str, title: str = '', model_id: str | None = None, advised_for_model_id: str | None = None, aspect: str = '16:9', resolution: str | None = None, shots: tuple[~artful.shot_schedule.ShotEntry, ...]=<factory>, risk_flags: tuple[~artful.shot_schedule.RiskFlag, ...]=<factory>, notes: str = '')[source]

The body of a shot-schedule annotation — the ordered shot list.

The annotation’s reference carries the asset_id this schedule plans, so it is not duplicated here (same split as artful.PanelBody).

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

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

property needs_advice: bool

True when the risk flags do not (or no longer) match model_id.

A schedule with no model_id cannot be advised, so it never needs advice; one whose model changed since it was advised always does.

shot(shot_id: str) ShotEntry | None[source]

The entry with shot_id, or None. Mirrors Storyboard.panel.

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_shot_schedule(store: IntervalAnnotationStore, *, asset_id: str, schedule_id: str | None = None, tier: str = 'shot-schedule') ShotScheduleBody | None[source]

One schedule, or None when there is no match.

With schedule_id given, returns that schedule. Without it, returns the first schedule found for asset_id / tier — convenient for the common one-schedule-per-asset case.

artful.load_shot_schedules(store: IntervalAnnotationStore, *, asset_id: str, tier: str = 'shot-schedule') list[ShotScheduleBody][source]

Every schedule in store for asset_id / tier, in store order.

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.new_schedule_id(prefix: str = 'sch') str[source]

Generate a fresh short schedule id (e.g. "sch3f7c1").

artful.new_shot_id(prefix: str = 'sh') str[source]

Generate a fresh short shot id (e.g. "sh3f7c1").

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_shot_schedule(schedule: ShotScheduleBody, store: IntervalAnnotationStore, *, asset_id: str, tier: str = 'shot-schedule', was_attributed_to: str = 'user:unknown', was_generated_by: str = 'agent:artful') Annotation[source]

Persist schedule into store as one timeless annotation.

Like artful.save_storyboard() this appends — saving twice adds a second annotation rather than replacing the first, so a store can hold a schedule’s revision history. Use tier (or schedule_id) to tell revisions apart on load.

Parameters:
  • schedule – The ShotScheduleBody to persist.

  • store – Any lacing.IntervalAnnotationStore.

  • asset_id – The asset this schedule plans (goes on the annotation’s reference, not into the body).

  • tier – Tier name for the persisted annotation.

  • was_generated_by (was_attributed_to /) – Provenance fields.

Returns:

The persisted lacing.Annotation.

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.