Metadata-Version: 2.4
Name: deck-spec
Version: 0.1.0
Summary: Typed, declarative Pydantic v2 model for slide decks with first-class JSON schema export for LLM structured output.
Project-URL: Homepage, https://github.com/tomastimelock/deck-spec
Project-URL: Repository, https://github.com/tomastimelock/deck-spec
Project-URL: Issues, https://github.com/tomastimelock/deck-spec/issues
Author-email: Trollfabriken AITrix AB <dev@trollfabriken.se>
License-Expression: MIT
License-File: LICENSE
Keywords: deck,json-schema,llm,presentation,pydantic,slides
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics :: Presentation
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pydantic>=2.5
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: jsonschema>=4; extra == 'dev'
Requires-Dist: pytest-cov>=4; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```python
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:

1. **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.
2. **Cross-language validation** — Node, browser, and Go can consume the schema
   without a Python runtime.
3. **Versioning** — `deck.schema.json` ships in the wheel, tagged with the
   package version. Pinning `deck-spec==0.1.0` locks 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:

```bash
deck-spec schema > deck.schema.json
deck-spec schema --pretty
```

Or load it in Python:

```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

```bash
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
