Typed, declarative Pydantic v2 model for slide decks with first-class JSON schema export for LLM structured output.
Project description
deck-spec
Typed, declarative Pydantic v2 model for slide decks. JSON schema included — LLMs target it directly.
Built at Trollfabriken AITrix AB for the AIMOS Insight and Granskning pipelines, where every product needed to emit narrated video, PDF handouts, and editable PowerPoint from a single LLM-authored source of truth. The package publishes the JSON schema; LLMs target it; validators catch errors before any renderer touches the data.
What it solves
| Previous problem | Solution |
|---|---|
| Every LLM-to-slides project invents its own ad-hoc JSON shape | One published schema; one Python model; one validation path |
python-pptx requires you to write layout code per slide (60+ lines/slide) |
Declarative Slide(layout="content", title=..., body=...) |
| LLM output drifts when prompts evolve | JSON schema constrains structured output across model versions |
| No way to verify a deck's shape before rendering | validate_json(...) raises with field path; render only if valid |
| Markdown-fenced JSON, trailing commas, prose preamble from LLMs | parse_response() handles all three quirks |
| Decks tied to one output format | One Deck -> PPTX, PDF, HTML, narrated MP4 (via siblings) |
| Swedish text breaks on naive serializers | UTF-8 round-trips; language="sv-SE" documented |
Installation
pip install deck-spec
pip install "deck-spec[dev]" # for testing
Runtime requirement: pydantic >= 2.5. Nothing else.
Quick start
from deck_spec import Deck, validate_json
# Validate JSON produced by an LLM
deck = validate_json('''
{
"title": "LVU for foraldrar",
"language": "sv-SE",
"slides": [
{"layout": "title",
"title": "LVU for foraldrar",
"subtitle": "Vad det ar och hur det fungerar"},
{"layout": "bullet",
"title": "Vad ar LVU?",
"bullets": [
"Lag med sarskilda bestammelser om vard av unga",
"Tvangsatgard nar frivilliga insatser inte racker",
"Beslut tas av forvaltningsratten"
]},
{"layout": "stat",
"title": "Omfattning",
"stat_value": "21 000",
"stat_label": "barn i samhallsvard",
"stat_supporting": "Sverige, senaste aret"}
]
}
''')
# Construct programmatically
from deck_spec import Slide, Theme
deck = Deck(
title="Q1 Review",
theme=Theme(name="default"),
slides=[
Slide(layout="title", title="Q1 Review", subtitle="2026"),
Slide(layout="stat", title="Revenue",
stat_value="$12.4M", stat_label="Q1", stat_supporting="+18% YoY"),
],
)
# Export the schema for LLM use
import json
from deck_spec import deck_schema
json.dump(deck_schema(), open("schema.json", "w"))
The schema
Why we publish the schema as a file
Three reasons:
- LLM structured output — OpenAI's
response_format, Anthropic's tool definitions, and JSON-mode all accept a JSON Schema. Pass the schema once; the model produces valid decks. - Cross-language validation — Node, browser, and Go can consume the schema without a Python runtime.
- Versioning —
deck.schema.jsonships in the wheel, tagged with the package version. Pinningdeck-spec==0.1.0locks the schema shape.
The schema is generated from the Pydantic models and checked into source control.
CI verifies they stay in sync (scripts/regen_and_check_schema.py).
Emit it with the CLI:
deck-spec schema > deck.schema.json
deck-spec schema --pretty
Or load it in Python:
from deck_spec import deck_schema
schema = deck_schema() # dict; pass directly to your LLM client
Configuration
Deck — top-level fields
| Field | Type | Default | Notes |
|---|---|---|---|
title |
str |
required | Deck title |
subtitle |
str | None |
None |
Optional subtitle |
author |
str | None |
None |
Author name |
language |
str |
"en" |
BCP-47; "sv-SE" supported |
theme |
Theme |
Theme() |
Visual theme; see below |
slides |
list[Slide] |
required | At least one slide expected |
metadata |
dict[str, str] |
{} |
Arbitrary string-keyed metadata |
voice_default |
VoiceConfig | None |
None |
Default TTS config; used by talk-cast |
Slide — key fields
| Field | Type | Default | Notes |
|---|---|---|---|
layout |
Literal[...] |
"content" |
Controls which content fields renderers read |
title |
str | None |
None |
Slide heading |
body |
str | None |
None |
Markdown text; "content" layout |
bullets |
list[str] | None |
None |
"bullet" layout |
columns |
list[Column] | None |
None |
"two_column" / "comparison" |
image |
ImageRef | None |
None |
"image" / "image_caption" |
quote |
str | None |
None |
"quote" layout |
stat_value |
str | None |
None |
"stat" layout — e.g. "$12.4M" |
notes |
str | None |
None |
Speaker notes; used as narration by talk-cast |
narration |
str | None |
None |
Explicit narration override |
transition |
Literal[...] |
"fade" |
"fade", "cut", "slide-left", "slide-right", "zoom" |
duration_hint |
float | None |
None |
Minimum seconds on screen (talk-cast) |
Available layouts
"title" — large centered title + subtitle
"section" — section divider on accent background
"content" — title + body text (most common)
"two_column" — title + two content columns
"bullet" — title + bulleted list
"image" — title + single large image
"image_caption" — image with caption beside it
"quote" — large pull quote with attribution
"comparison" — two side-by-side blocks
"stat" — one big number, label, and supporting line
"blank" — custom; place elements explicitly
Theme
| Field | Type | Default |
|---|---|---|
name |
str |
"default" |
width_px |
int |
1920 |
height_px |
int |
1080 |
margin_px |
int |
80 |
accent_style |
Literal[...] |
"solid" |
colors |
ColorPalette |
see below |
fonts |
FontConfig |
see below |
ColorPalette defaults: background="#FFFFFF", foreground="#1A1A1A",
accent1="#0B5FFF", accent2="#FF6B35", accent3="#00A878".
FontConfig defaults: heading="Inter", body="Inter", mono="JetBrains Mono",
base_size_pt=18.
CLI
deck-spec schema # print JSON schema to stdout
deck-spec schema --pretty # pretty-printed
deck-spec validate deck.json # validate file; exit 0 if valid
deck-spec inspect deck.json # slide count, layouts, missing narration
deck-spec example > example.json # write a reference example
deck-spec example --layout bullet # example focused on bullet layout
validate prints the Pydantic field path on failure and exits 1. Use it in
pre-render pipelines to gate bad LLM output before it reaches a renderer.
inspect does not validate — it reports structure. Useful for debugging a deck
that passes schema validation but looks wrong in output.
Package structure
deck-spec/
├── src/
│ └── deck_spec/
│ ├── __init__.py <- version, public exports
│ ├── models.py <- Deck, Slide, Theme, Element, ...
│ ├── colors.py <- ColorPalette + named-palette presets
│ ├── layouts.py <- layout enum + field-relevance metadata
│ ├── validate.py <- validate_json, validate_dict, decks_equivalent
│ ├── cli.py <- argparse CLI (deck-spec entrypoint)
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── deck.schema.json <- generated; checked in; ships in wheel
│ ├── llm/
│ │ ├── __init__.py
│ │ ├── prompts.py <- build_prompt, parse_response
│ │ └── examples.py <- example Deck instances
│ └── examples/
│ ├── education.json <- civic education example deck
│ ├── audit.json <- municipal audit report deck
│ └── civic.json <- LVU/socialtjanst explainer deck
├── tests/
│ ├── test_model_validation.py
│ ├── test_schema_generation.py
│ ├── test_schema_round_trip.py
│ ├── test_validate_helpers.py
│ ├── test_llm_helpers.py
│ ├── test_cli_schema.py
│ ├── test_cli_validate.py
│ └── test_examples_validate.py
├── scripts/
│ └── regen_and_check_schema.py <- update deck.schema.json; CI checks sync
├── pyproject.toml
├── MANIFEST.in
└── LICENSE
(C) Trollfabriken AITrix AB -- MIT licensed
Project details
Release history Release notifications | RSS feed
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 deck_spec-0.1.0.tar.gz.
File metadata
- Download URL: deck_spec-0.1.0.tar.gz
- Upload date:
- Size: 18.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54bbca9565f40e8996320364ea8234b74c322875cc0984d601df7dec207eac23
|
|
| MD5 |
490138f75229c72ad35a28357f5d9bf3
|
|
| BLAKE2b-256 |
92eebf7ad0dd3737268e8e2b3db0f9e71cdaadd6fc28acea327dde5bd5c26cb6
|
Provenance
The following attestation bundles were made for deck_spec-0.1.0.tar.gz:
Publisher:
release.yml on tomastimelock/deck-spec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deck_spec-0.1.0.tar.gz -
Subject digest:
54bbca9565f40e8996320364ea8234b74c322875cc0984d601df7dec207eac23 - Sigstore transparency entry: 1602080559
- Sigstore integration time:
-
Permalink:
tomastimelock/deck-spec@9a50b0d3be1b89c86c5592fe52ca7b1e3a55c617 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tomastimelock
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9a50b0d3be1b89c86c5592fe52ca7b1e3a55c617 -
Trigger Event:
push
-
Statement type:
File details
Details for the file deck_spec-0.1.0-py3-none-any.whl.
File metadata
- Download URL: deck_spec-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b71572967d297e0f2ecdf45b031a366c8f78ee870d341ba68229222bd2dcceed
|
|
| MD5 |
0427f9ba24abb54180e964f1002da30c
|
|
| BLAKE2b-256 |
bae06da406ae840289bb0efc05d313c2e9cfb19d71cfaeef80f0d8f9a5d9bbc6
|
Provenance
The following attestation bundles were made for deck_spec-0.1.0-py3-none-any.whl:
Publisher:
release.yml on tomastimelock/deck-spec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deck_spec-0.1.0-py3-none-any.whl -
Subject digest:
b71572967d297e0f2ecdf45b031a366c8f78ee870d341ba68229222bd2dcceed - Sigstore transparency entry: 1602080570
- Sigstore integration time:
-
Permalink:
tomastimelock/deck-spec@9a50b0d3be1b89c86c5592fe52ca7b1e3a55c617 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tomastimelock
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9a50b0d3be1b89c86c5592fe52ca7b1e3a55c617 -
Trigger Event:
push
-
Statement type: