artful
Storyboard data model and exporters — lacing-native panels along a timeline.
A storyboard is a sequence of panels along a timeline (a song, a video, a podcast clip). Each panel pins an interval of the master asset, optionally points at a project shot, and carries one or more images plus directorial annotations (caption, framing, camera, transition, notes). Panels are persisted as lacing annotations, so a storyboard is queryable, exportable, and round-trippable through every adapter lacing already supports (TextGrid, EAF, JAMS, OTIO, WebVTT, Web Annotation, …) without artful having to reinvent any of it.
artful is not the renderer. It's the panels-along-a-timeline plan that drives a renderer: an LLM authors panels from a script, a human reviews the contact sheet, the same panel data feeds downstream image / video generation.
Install
pip install artful
Requires pydantic>=2.6 and lacing>=0.0.13.
Quick start
from artful import (
PanelBody,
PanelImage,
Storyboard,
panel_intervals_from_panels,
save_storyboard,
load_storyboard,
)
from lacing import MemoryStore
sb = Storyboard(
title="The Bells — v1",
asset_id="song-asset-id-abc",
style="noir, candlelight",
panels=(
PanelBody(
panel_id="p1",
caption="Thor at the piano",
framing="medium",
images=(PanelImage(path="composite.png", role="seed"),),
),
PanelBody(
panel_id="p2",
caption="Bells over winter sky",
framing="wide",
camera="slow push-in",
),
),
)
# Pin each panel to a time interval (seconds on the master timeline).
intervals = panel_intervals_from_panels(
[
("p1", 0.0, 4.0),
("p2", 4.0, 8.0),
]
)
store = MemoryStore()
save_storyboard(sb, store, panel_intervals=intervals)
# ... later, possibly in a different process / backend ...
loaded = load_storyboard(store, asset_id="song-asset-id-abc")
The store can be any lacing.IntervalAnnotationStore (MemoryStore,
SQLiteStore, PostgresStore, …); the persistence layer is lacing's.
Authoring with LLMs (Markdown)
Markdown is artful's canonical "give an LLM a storyboard to read or write"
format. It round-trips losslessly through to_markdown / from_markdown.
from artful import to_markdown, from_markdown
md = to_markdown(loaded, intervals)
# Show `md` to an LLM, let it edit panels, then…
edited_sb, edited_intervals = from_markdown(edited_md)
save_storyboard(edited_sb, store, panel_intervals=edited_intervals)
The Markdown shape:
# The Bells — v1
- asset_id: `song-asset-id-abc`
- style: noir, candlelight
- aspect: 16:9
## panel p1 [0.00..4.00]s
- framing: medium
Thor at the piano

## panel p2 [4.00..8.00]s
- framing: wide
- camera: slow push-in
Bells over winter sky
Reviewing as a contact sheet (HTML)
from artful import to_html
with open("storyboard.html", "w") as f:
f.write(to_html(loaded, intervals))
to_html produces a self-contained HTML page with embedded styles and
<img> tags pointing at each panel's path / url. Open it in a browser
to review.
Data model
| Type | What it is |
|---|---|
Storyboard |
Title, asset_id (the timeline), panels, style hint, aspect ratio. |
PanelBody |
One panel: panel_id, optional shot_id, images, caption, framing, camera, transition_in, notes. The interval lives on the lacing annotation's reference, not in the body. |
PanelImage |
One image for a panel — artifact_id (sha-256 lacing artifact ref), url, or path, plus a role (thumbnail | seed | reference | alternate) and caption. |
All three are frozen Pydantic models with extra="forbid". Importing
artful registers PanelBody against lacing under the body-schema URI
annot://schema/storyboard-panel/v1, so lacing.validate_body(...) works
out of the box.
Why lacing-native?
Storyboards are an annotation problem: panels are intervals on a timeline
with structured bodies. Rather than invent a new persistence layer, artful
encodes each panel as a lacing.Annotation whose body_schema_uri is the
panel schema. The benefits:
- Allen interval algebra for free: overlaps, gaps, adjacencies.
- Multiple storyboards over the same asset stay separate via the
tierfield on each annotation. - Provenance tracked across edits through lacing's
Provenancemodel. - Format adapters — read or write panels as TextGrid tiers, JAMS annotations, OTIO clips, Web Annotation JSON-LD, etc., via lacing.
A small StoryboardMetaBody schema (URI
annot://schema/storyboard-meta/v1) carries title / style / aspect as a
single timeless annotation alongside the panels.
The shot schedule
Before there are panels there is a shot schedule: an ordered list of shots, each carrying the constraints a downstream planner must respect, and the advisory risk flags raised when those constraints collide with the chosen video model's real limits.
from artful import (
RiskFlag,
ShotEntry,
ShotScheduleBody,
save_shot_schedule,
load_shot_schedule,
)
sched = ShotScheduleBody(
schedule_id="sch-001",
title="Scene 4 — the pub",
model_id="fal-ai/bytedance/seedance/v1/pro/image-to-video", # a reference
aspect="16:9",
resolution="720p",
shots=(
ShotEntry(
shot_id="sh-01",
description="Mairead pushes the door open.",
characters=("Mairead",),
shot_size="MS",
duration_seconds_estimate=6.0,
max_duration_seconds=8.0,
max_characters_in_frame=1,
take_budget=3,
clump_id="pub-interior",
),
ShotEntry(
shot_id="sh-02",
characters=("Mairead", "Declan"),
duration_seconds_estimate=14.0,
has_dialogue=True,
risk_flags=(
RiskFlag(
code="over_clip_cap",
message="~14s is over this model's ~10s clip cap.",
gotcha_id="seedance-clip-length-cap",
),
),
),
),
)
save_shot_schedule(sched, store, asset_id="song-asset-id-abc")
loaded = load_shot_schedule(store, asset_id="song-asset-id-abc")
Three sources of truth, deliberately kept apart:
| what | who owns it |
|---|---|
What a model can do (max_clip_seconds, single_character_recommended, supported_resolutions, …) |
the model registry downstream — referenced here by model_id, never copied. |
What a shot requires (max_duration_seconds, max_characters_in_frame, aspect, resolution, take_budget) |
ShotEntry. |
| What happens when the two collide | RiskFlag — a cached verdict, stamped with advised_for_model_id so a model change makes it visibly stale via schedule.needs_advice. |
Ordering is the tuple order of shots — there is deliberately no order
field to disagree with it, and no character_count field to drift from
len(characters). Constraint defaults encode the safe choice:
allow_last_frame_anchor is False, because anchoring both a first and a
last frame contorts the subject mid-clip.
The schedule persists as a single timeless annotation under
annot://schema/shot-schedule/v1 — a schedule exists before times are
pinned, which is exactly what duration_seconds_estimate is for.
API surface
from artful import (
# data model
Storyboard,
PanelBody,
PanelImage,
ModelSheet,
ShotScheduleBody,
ShotEntry,
RiskFlag,
new_panel_id,
new_shot_id,
new_schedule_id,
# persistence (round-trip with any lacing.IntervalAnnotationStore)
save_storyboard,
load_storyboard,
panel_intervals_from_panels,
save_shot_schedule,
load_shot_schedule,
load_shot_schedules,
# exports
to_markdown,
from_markdown,
to_html,
# body-schema URIs
PANEL_BODY_SCHEMA_URI,
STORYBOARD_META_BODY_SCHEMA_URI,
MODEL_SHEET_BODY_SCHEMA_URI,
SHOT_SCHEDULE_BODY_SCHEMA_URI,
StoryboardMetaBody,
)
PDF export is deferred to the optional [pdf] extra (reportlab).
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file artful-0.0.9.tar.gz.
File metadata
- Download URL: artful-0.0.9.tar.gz
- Upload date:
- Size: 33.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e894aa2a429b57a8d6d345a0a7fee033d032b389e7301c6ca329514c8fcb71af
|
|
| MD5 |
4eb2fcd1dd1255960884a5434c11f5c4
|
|
| BLAKE2b-256 |
e0c27e40c1de7c0ebe34d86c1d012fc5409c6b4fb069546ef8ff070f0928464a
|
File details
Details for the file artful-0.0.9-py3-none-any.whl.
File metadata
- Download URL: artful-0.0.9-py3-none-any.whl
- Upload date:
- Size: 24.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
773c94cdf2542e62d5c3ddf7f3f85b8be781cadcb320698dc8f006b94c46e0c4
|
|
| MD5 |
25a03f4989ba52c5cca0a530e93a8391
|
|
| BLAKE2b-256 |
fd41e695b2f02a979f9427490fba0ae29ee4961d48fa5a7bfa54c2ba486fbf82
|