Carousels-as-code — generate brand-consistent social-media carousels from JSON specs.
Project description
English · Русский
kalve
Carousels-as-code — no Canva, no Figma, just JSON.
Generate brand-consistent Instagram and Threads carousels from a JSON spec. Python, offline, MIT-licensed. Composable with any AI assistant (Claude, ChatGPT, Cursor). Think of it as a self-hosted, pip install-able Bannerbear — but for devs who'd rather edit a config file than click through a dashboard.
Built for Šaunuolė (a Lithuanian language tutor) but designed as a universal tool: plug in your own brand kit, get cards in your own style.
See it in action
Same JSON spec, three different brand kits — that's the point:
default_dark — developer-native dark theme:
default_light — clean minimal light theme:
saunuole — a real brand (Lithuanian language tutor):
Who's this for
You, if you're an indie hacker, solo founder, or a small team that:
- ships your own product and runs your own social media
- is tired of hand-crafting posts in Canva or Figma every time you need to publish
- wants brand-consistent carousels without hiring a designer
- already thinks in JSON and loves
-as-codeworkflows (GitOps, Infrastructure-as-Code, and now Carousels-as-Code)
Configure a brand kit once — from that point every new carousel is one python -m kalve spec.json away. kalve pipes into any workflow (N8N, cron, GitHub Actions, a bash one-liner) and composes cleanly with any AI of choice — see docs/AI_INTEGRATION.md for a ready-to-paste system prompt.
What it does
- Templates as code. Describe a card structure once — render unlimited variations by swapping content.
- Brand kit separate from template. Colors, fonts, radii in a single Python dataclass. Change the brand kit → entire visual language changes. Same JSON spec produces cards in any brand.
- CLI and Python API. One command from the terminal, or an import for your pipeline.
- No AI inside. kalve only renders pixels. AI-driven content generation lives above it — bring your own LLM, your own API keys, your own prompts. See AI_INTEGRATION.md for the ready-made recipe.
- Ready-to-post output. 1080×1350 PNG, sized for Instagram and Threads carousels.
- Offline and deterministic. No network calls, no telemetry, no surprises. Same input → same output, every time.
v0.1 ships with one template — three_tiers (a 4-card carousel showing "3 skill levels + CTA"). More templates are coming (see roadmap).
Example
Input (examples/saunuole_compliments.json):
{
"brand": "saunuole",
"template": "three_tiers",
"spec": {
"topic": "Compliments about food in a restaurant",
"emoji": "🍽️",
"tiers": ["turistas", "vietinis", "lietuvis"],
"tier_labels_lt": ["Turistas", "Vietinis", "Lietuvis"],
"tier_labels_native": ["tourist", "local", "Lithuanian"],
"cards": [
{"lt": "Labai skanu!", "native": "Very tasty!"},
{"lt": "Patiekalas buvo tiesiog nuostabus.",
"native": "The dish was simply wonderful."},
{"lt": "Net pirštus galima aplaižyti!",
"native": "You could lick your fingers!"}
]
}
}
Output: four 1080×1350 PNG files, ready to post.
Install
Requires Python 3.9+.
pip install kalve
Or from source:
git clone https://github.com/anna-ladutko/kalve.git
cd kalve
pip install -r requirements.txt
Fonts (Inter) ship with the package — no extra download needed.
Usage
CLI
python -m kalve examples/saunuole_compliments.json --output output/
Result: four PNG files in output/.
Python API
from kalve import generate
from kalve.brands.saunuole import SAUNUOLE
generate(
spec={
"topic": "Compliments about food",
"emoji": "🍽️",
"tiers": ["turistas", "vietinis", "lietuvis"],
"tier_labels_lt": ["Turistas", "Vietinis", "Lietuvis"],
"tier_labels_native": ["tourist", "local", "Lithuanian"],
"cards": [
{"lt": "Labai skanu!", "native": "Very tasty!"},
{"lt": "Patiekalas buvo nuostabus.", "native": "The dish was wonderful."},
{"lt": "Net pirštus galima aplaižyti!", "native": "Lick your fingers good!"},
],
},
brand=SAUNUOLE,
template="three_tiers",
output_dir="output/",
)
With AI (your personal copywriter)
kalve is designed to pair with an AI assistant. Chat with Claude / ChatGPT / Cursor in natural language, get a valid JSON spec back, pipe it into kalve. A ready-to-paste system prompt and worked examples live in docs/AI_INTEGRATION.md.
Architecture
Three independent layers — any two can change without breaking the third:
- Engine (
kalve/drawing.py,kalve/typography.py) — low-level primitives: gradients, rounded rectangles, pill badges, fonts, emoji paste. Rarely touched. - Template (
kalve/templates/three_tiers.py) — layout logic. What goes where on a card. Reads from the brand kit; never hardcodes colors. - Brand kit (
kalve/brands/saunuole.py) — pure data. Colors, fonts, margins, tier palettes. No logic.
More detail in docs/ARCHITECTURE.md.
Making your own brand kit
Copy kalve/brands/saunuole.py, change the values, register the import. Full step-by-step guide: docs/BRAND_KIT_GUIDE.md.
Extending kalve
Add a new template (Stories, OG images, ad banners), a new brand kit, or a new engine primitive — all covered in CONTRIBUTING.md.
Repository structure
kalve/
├── kalve/ Main package
│ ├── __init__.py Public API: generate()
│ ├── __main__.py CLI entry point
│ ├── brand.py BrandKit dataclass
│ ├── drawing.py Engine: gradients, rects, pills, emoji
│ ├── typography.py Engine: fonts, text wrapping, auto-sizing
│ ├── colors.py Engine: color helpers
│ ├── brands/ Brand kits (data)
│ │ └── saunuole.py Reference brand
│ └── templates/ Card layouts (logic)
│ └── three_tiers.py "3 skill levels + CTA" carousel
│
├── kalve/assets/fonts/ Inter font family (shipped with the package)
├── examples/ Sample JSON specs and rendered PNGs
│
├── docs/
│ ├── ARCHITECTURE.md Why three layers, how they compose
│ ├── BRAND_KIT_GUIDE.md How to describe your own brand
│ └── AI_INTEGRATION.md Using kalve with Claude/ChatGPT/Cursor
│
├── CLAUDE.md Context for AI assistants opening this repo
└── CONTRIBUTING.md How to add brand kits, templates, primitives
Roadmap
-
google_adstemplate — 10 ad sizes from a single spec -
og_imagetemplate — Open Graph images for blogs and landing pages -
storytemplate — vertical 1080×1920 stories for Instagram/Facebook - Custom fonts in brand kits (not just Inter)
- Batch mode — many specs in, many carousels out
- Spec validation with human-readable error messages
License
MIT. Do whatever you want with the code. The bundled Inter font is licensed separately under the SIL Open Font License 1.1.
Why "kalve"
Kalvė is Lithuanian for "forge" — the place where raw material (a JSON spec) is hammered into a finished product (branded PNGs) with a single command.
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 kalve-0.1.1.tar.gz.
File metadata
- Download URL: kalve-0.1.1.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea10c5169f650b9136fb461f29b5b44d8bdb8046d0ba2d685d185484c8c1e71a
|
|
| MD5 |
3b42ae62a8b49a4d3df45d03695307d8
|
|
| BLAKE2b-256 |
aca17abea32381d9954c8d4da3b98a414f07c97974403d7b9785876064da47e2
|
File details
Details for the file kalve-0.1.1-py3-none-any.whl.
File metadata
- Download URL: kalve-0.1.1-py3-none-any.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75116d4ca346a59928128f2fd3402e0ab734464ce031edcba8ae40c36747dc85
|
|
| MD5 |
35e250a2f944758b2f689c2b43ddaa6c
|
|
| BLAKE2b-256 |
ac3f2adf4e2f6eb5b14151b9f82405d2212514bb63228f6aa21e1b8df460d813
|