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:
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.ShotScheduleBody,ShotEntry,RiskFlag+save_shot_schedule()/load_shot_schedule()— the shot schedule and its store round-trip.
- 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.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_advicesays 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.shotsis the shot’s order — there is deliberately noorderfield 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
referencecarries theasset_idthis schedule plans, so it is not duplicated here (same split asartful.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_idcannot be advised, so it never needs advice; one whose model changed since it was advised always does.
- 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_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_idgiven, returns that schedule. Without it, returns the first schedule found forasset_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
storeforasset_id/tier, in store order.
- 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.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
scheduleintostoreas 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. Usetier(orschedule_id) to tell revisions apart on load.- Parameters:
schedule – The
ShotScheduleBodyto 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
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.