CCV4, a new character card format
CCV4 is a draft format for AI character cards, meant to replace the SillyTavern v1, v2 and v3 formats. A card is a normal PNG. The artwork previews everywhere, and the card data travels inside the file: TOML text in private chunks, media assets as raw bytes next to it.
Everything a card says is a text block: a small TOML table addressed by type and id. Type names are singular and telling: name, description, personality, wardrobe, scenario, greeting, prompt, lore.
[description.esmeralda] text = "A dancer on the square of Notre Dame, admired by all of Paris ..."
A character is a flat combination of blocks. No inheritance, no defaults, no hidden magic. Its prompt list is the full prompt template, sent to the LLM in exactly that order. Lowercase strings reference blocks, one liners can stay inline, and UPPERCASE entries are placeholders the app fills.
[character.esmeralda]
name-card = "Esmeralda" # label in the app's card list
name-char = "name.esmeralda" # what {{char}} resolves to
greetings = ["greeting.coin", "greeting.knife"]
prompt = [
"name.esmeralda",
"description.esmeralda",
"personality.esmeralda",
{ text = "Paris, 1482, at night." },
"CHAT-HISTORY",
]
The picked greeting is simply the first message of the chat. There is no greeting slot.
Cards reference blocks, they do not own them. Two cards can point at the same description: edit it once and both change. One file can hold a whole cast, parallel language versions of any block, and the same person on the best day of her year or on the worst.
[character.esmeralda-festival] prompt = [ "description.esmeralda", "wardrobe.festival", "CHAT-HISTORY" ] [character.esmeralda-sanctuary] prompt = [ "description.esmeralda", "wardrobe.sanctuary", "CHAT-HISTORY" ]
A lore block wakes up when one of its keywords appears in the chat. A lorebook bundles lore blocks, and the LOREBOOK placeholder marks where triggered entries land in the prompt. Characters can share a lorebook the same way they share any block.
[lore.shadow] keywords = ["priest", "shadow", "cathedral"] text = "A priest watches the square from the cathedral, every single day." [lorebook.old-paris] entries = ["lore.shadow"]
Every chunk filename starts with card/ for authored work or user/ for personal data like chat sessions. Sharing a card means copying the PNG and dropping the user/ chunks. No parsing, no rebuild, your chats never leave your machine.
esmeralda.png ├─ IHDR, IDAT, IEND standard PNG, the visible artwork ├─ toMl card/character.toml the cards, blocks and prompts ├─ toMl card/lore.toml lorebooks and lore blocks ├─ asEt card/assets/voice.ogg raw asset bytes, referenced as asset:// └─ toMl user/session-01.toml your chat, dropped when you share ✂
Every CCV4 chunk is a private PNG chunk: toMl for a TOML file, asEt for one binary asset. Each carries a filename, and the card/ or user/ prefix decides what stays when you share. A viewer that does not know CCV4 ignores every one of them and still shows the picture.
Best way to get a feel for it: edit the eight examples live in the playground.
A card is only data. Turning it into a prompt is one small, deterministic procedure, and every app runs the same one. Here it is start to finish. The playground runs exactly this on the eight examples.
esmeralda.png
|
| read the private chunks
v
toMl chunks --parse--> TOML tables (the text blocks)
asEt chunks ----------> raw asset bytes (what asset:// points at)
|
v
pick one [character.id]
|
v
walk its prompt list, in order:
|
| "type.id" -> look up the block, expand macros, place its text
| { text = "..." } -> place the inline prose as written
| CHAT-HISTORY -> the greeting, then any depth prompts sunk into it
| LOREBOOK -> the lore entries whose keywords fired
v
assembled prompt -> the LLM
[character.id]. Its prompt list is the entire template. Nothing outside that list is sent.{{char}} becomes the text of the block named by name-char, or the character id when that block is missing. {{user}} becomes the active persona name. No other macros exist.CHAT-HISTORY and LOREBOOK are the two the format defines. Any other uppercase token is left for the app.Every entry in prompt is exactly one of these:
"description.esmeralda" reference: look it up, place its text
{ text = "Paris, 1482." } inline: place the prose as written
CHAT-HISTORY placeholder: the app fills the slot
A reference is type.id, split on the first dot. A broken reference is always an error. In any text, {{user}} and {{char}} are expanded and nothing else is touched.
There is no greeting slot. The card lists greetings (or group-greetings in a group), the user picks one, and it is simply the first message inside CHAT-HISTORY. The conversation grows from there.
A prompt block can carry depth = N. It leaves its written position and is injected N messages before the end of the history, so a standing instruction stays next to the latest turn instead of drifting out of reach.
A lore block wakes when one of its keywords shows up in the recent chat. LOREBOOK marks where the woken entries land. The books in effect are the card's own plus any from a group it belongs to, so a shared world is written once.
That is the whole algorithm. It lives in one small module, ccv4-engine.js, that the playground runs directly, so there is a single behavior and no second copy to drift.
The same engine and the validator ship as the ccv4 package. Install it from GitHub (no npm account needed):
npm install github:elana-voss/ccv4
Assemble a prompt, or validate a card in relaxed or strict mode. You bring the parsed TOML; parsing is the caller's job.
import { assemble, validate } from "ccv4";
const result = assemble(doc, { charId: "esmeralda-festival", user: "Gringoire" });
const report = validate(doc, { mode: "strict" });
There is a command line too:
ccv4 validate card.toml --strict ccv4 assemble card.toml --character esmeralda-festival
Loading specification…
Loading example…