Zero-dependency parser/writer for SillyTavern character cards (V1/V2/V3 JSON + PNG tEXt/zTXt embedding)
Project description
tavern-card
Zero-dependency parser/writer for SillyTavern character cards — V1/V2/V3 JSON plus PNG tEXt/zTXt embedding, with built-in protection against decompression bombs.
- No dependencies. Pure standard library (
base64,json,struct,zlib). - Every card shape → one canonical V2 dict. Flat V1, nested V2/V3, JSON, base64, or a PNG image.
- Round-trips through PNG. Read a card out of a PNG, write one back in.
- Hardened. Explicit size ceilings on file, chunk, and decompressed zTXt output.
Install
pip install tavern-card
Requires Python 3.10+.
Quickstart
Read a card embedded in a PNG:
from tavern_card import parse_png_card
with open("character.png", "rb") as f:
card = parse_png_card(f.read())
print(card["data"]["name"])
print(card["spec"]) # "chara_card_v2" or "chara_card_v3"
Write a card into a PNG:
from tavern_card import parse_card, write_png_card
card = parse_card({
"spec": "chara_card_v2",
"data": {"name": "Nyx", "description": "A wandering star."},
})
# Uses a 1x1 transparent base; pass template_png=<bytes> to embed into your own image.
png_bytes = write_png_card(card)
with open("nyx.png", "wb") as f:
f.write(png_bytes)
Normalize any JSON shape to V2:
from tavern_card import parse_card
# Flat V1, nested V2/V3, a JSON string, base64, or raw bytes — all accepted.
card = parse_card({"name": "Bob", "char_persona": "A tired knight."})
assert card["spec"] == "chara_card_v1"
assert card["data"]["name"] == "Bob"
assert card["data"]["description"] == "A tired knight."
API
| Function | Signature | Description |
|---|---|---|
parse_card |
parse_card(data: dict | str | bytes) -> dict |
Normalize a dict, JSON string, base64 string, or raw bytes into the canonical V2 dict. Unwraps common {"card": {...}} / {"character": {...}} envelopes. |
parse_png_card |
parse_png_card(blob: bytes) -> dict |
Extract and parse the card embedded in a PNG's tEXt/zTXt chunks (ccv3 preferred over chara). |
write_png_card |
write_png_card(v2_card: dict, template_png: bytes | None = None) -> bytes |
Embed a V2 card into a PNG chara tEXt chunk, optionally using your own PNG as the base image. |
Every function returns (or accepts) the same three-layer V2 shape:
{
"spec": "chara_card_v2", # or "chara_card_v3" / "chara_card_v1"
"spec_version": "2.0",
"data": {
"name": str, "description": str, "personality": str, "scenario": str,
"first_mes": str, "mes_example": str, "creator_notes": str,
"system_prompt": str, "post_history_instructions": str,
"alternate_greetings": list, "tags": list, "creator": str,
"character_version": str, "extensions": dict, "character_book": Any,
},
}
Command line
Installs a tavern-card console script (pure argparse):
# Read a PNG or JSON card and print pretty JSON to stdout.
tavern-card inspect character.png
# Convert between .png and .json (direction inferred from file extensions).
tavern-card convert character.png character.json
tavern-card convert character.json character.png --template base.png
PNG format
SillyTavern stores the character-card JSON — base64-encoded — inside a PNG text chunk. tavern-card reads and writes exactly that layout:
| Aspect | Behavior |
|---|---|
| Chunk keys | ccv3 (V3) is tried first, then chara (V1/V2). |
| Chunk types | tEXt (uncompressed) and zTXt (zlib-deflated) are both read; writes always use tEXt. |
| Payload | Base64-encoded UTF-8 JSON; raw JSON is also accepted on read. |
| Insertion | On write, the chara chunk is inserted immediately before IEND. |
Safety limits
Parsing untrusted images is bounded to defeat memory-exhaustion and zlib-bomb attacks:
| Limit | Value | Rationale |
|---|---|---|
| Whole PNG file | 10 MB | A single card image cannot exhaust the process. |
| Single chunk length | 8 MB | A declared chunk length is rejected before allocation. |
| zTXt decompressed | 4 MB | Streaming decompression aborts the instant output crosses the ceiling, so a tiny compressed payload can't inflate into gigabytes. |
Exceeding any limit raises ValueError rather than allocating.
Roadmap / Contributing
Contributions welcome. Two good first issues:
- WebP support — SillyTavern also ships cards embedded in WebP; add
parse_webp_card/write_webp_cardmirroring the PNG path. - charx (V3 zip) support — the V3 spec defines a
.charxcontainer (a zip withcard.jsonplus assets); add read/write for it.
Please keep the library dependency-free and preserve the existing parsing behavior (there is a test suite covering round-trips and the safety limits).
License
MIT — see LICENSE.
Acknowledgements
Extracted from the RPG Roleplay platform (https://github.com/felixchaos/rpg-roleplay-platform).
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 tavern_card-0.1.1.tar.gz.
File metadata
- Download URL: tavern_card-0.1.1.tar.gz
- Upload date:
- Size: 14.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23abeb6fcded68de800bb19c214520378e7b961c191f22371cf1263952700408
|
|
| MD5 |
9131135bfdf5829ab3ed92121abf9eaa
|
|
| BLAKE2b-256 |
618e5dec12b07c1d780b62bd96250a591386bbbe155f8f36da928b13d03acd47
|
File details
Details for the file tavern_card-0.1.1-py3-none-any.whl.
File metadata
- Download URL: tavern_card-0.1.1-py3-none-any.whl
- Upload date:
- Size: 10.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b910eaa947fca4440b736136ac38ba2a77d2d72aa1e6ab621afc7937305b6d1
|
|
| MD5 |
2541af3365817e5ebae3b7e0be458ab3
|
|
| BLAKE2b-256 |
deccd681f0f966d900385a4720d8c2b2286abe6a24ac0e6dfa8ac56f7b10510c
|