Metadata-Version: 2.4
Name: tavern-card
Version: 0.1.1
Summary: Zero-dependency parser/writer for SillyTavern character cards (V1/V2/V3 JSON + PNG tEXt/zTXt embedding)
Project-URL: Homepage, https://github.com/felixchaos/tavern-card
Project-URL: Issues, https://github.com/felixchaos/tavern-card/issues
Author: Stellatrix Labs
License-Expression: MIT
License-File: LICENSE
Keywords: ccv3,chara,character-card,png,sillytavern,tavern
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 :: Software Development :: Libraries
Classifier: Topic :: Text Processing
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# tavern-card

Zero-dependency parser/writer for [SillyTavern](https://github.com/SillyTavern/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

```bash
pip install tavern-card
```

Requires Python 3.10+.

## Quickstart

**Read a card embedded in a PNG:**

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

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

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

```python
{
    "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`):

```bash
# 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_card` mirroring the PNG path.
- **charx (V3 zip) support** — the V3 spec defines a `.charx` container (a zip with `card.json` plus 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](LICENSE).

## Acknowledgements

Extracted from the RPG Roleplay platform (https://github.com/felixchaos/rpg-roleplay-platform).
