Skip to content
Documentation

Character cards

Tacita's character-card subsystem in detail. The parser is pure Dart, the field map is exhaustive, and round-trip preservation is part of the test suite.

What Tacita imports

Tacita imports the community-standard CCv2, CCv3, and CHARX character-card envelopes plus a raw JSON form. The parser is a pure function — given the bytes the user picked, it produces a typed Persona plus a list of advisories. The PNG and APNG carriers are walked in pure Dart with no pixel decode and no extra dependency.

Tacita does not browse, recommend, host, or distribute character files. It opens files the user already has on their device.

Format Carrier Picked when…
CCv3 PNG / APNG tEXt keyword ccv3 Preferred when both CCv2 and CCv3 chunks exist
CCv2 PNG / APNG tEXt keyword chara Only the chara chunk is present
CHARX ZIP with card.json at root + assets/ Magic bytes PK…
Raw JSON UTF-8 envelope Starts with {, declares spec chara_card_v2 or chara_card_v3

For the PNG / APNG carriers, the chunk value is base64(utf8(JSON)). Tacita accepts both tEXt and uncompressed iTXt. Compressed iTXt is rare in the wild; we deliberately do not pull in zlib for this code path.

Field map

Every documented spec field maps to a discrete persona field. No merging, no concatenation. The system-prompt builder composes the fields at use-site, separately, so descriptions and personalities do not bleed into each other.

Spec Persona field Notes
data.namenamerequired, capped 80 chars
data.descriptiondescriptioncapped 16 KB
data.personalitypersonalityseparate from description
data.scenarioscenarioper-chat scenario still wins
data.first_mesfirstMessagecapped 8 KB
data.alternate_greetings[]alternateGreetingsup to 32
data.mes_examplemessageExample<START> separators preserved
data.creator_notescreatorNotescapped 4 KB
data.creatorcreator
data.character_versioncharacterVersionopaque string
data.tags[]tagsup to 64, each ≤ 32 chars
data.system_promptinstructionscapped 8 KB
data.post_history_instructionspostHistoryInstructionsseparate
data.character_bookcharacterBookfull lorebook (see below)
data.nickname (v3)nickname
data.source[] (v3)sourceUrlsrendered as inert text — never opened on tap
data.group_only_greetings[] (v3)groupOnlyGreetingsreserved for group chat
data.creation_date (v3)creationDateUnix
data.modification_date (v3)modificationDateUnix
data.creator_notes_multilingual (v3)creatorNotesMultilingual
data.assets[] (v3)assetsbytes go through encrypted BlobStore
data.extensionsextensions + hiddenExtensionsvoice keys split out

Imported cards default to Roleplay mode. The user can switch to Chat in the preview UI; Assistant mode is intentionally disabled for cards because the format is not a fit for that mode.

Lorebook activation algorithm

The lorebook engine is a pure function. Given a book and the last book.scanDepth messages of the chat, it returns the entries to inject, split into beforeChar and afterChar buckets.

  1. Always include enabled && constant entries.
  2. For each non-constant enabled entry, scan the joined messages for any of keys (case-insensitive unless caseSensitive).
  3. If the entry is selective, also require any of secondaryKeys in the same window.
  4. If the book has recursiveScanning, append the matched contents to the corpus and re-run steps 2–3 once. Bounded to one pass to prevent infinite loops.
  5. Substitute {{char}} and {{user}} macros.
  6. Sort by priority descending (null → 100), then insertion_order ascending.
  7. Greedy budget fill up to tokenBudget × 4 chars (default ~2000 chars). Atomic — entries are never partially truncated.
  8. Split into beforeChar and afterChar, preserving the sort order.

Sanitisation

Stripped silently from every imported text field — description, personality, scenario, first message, alternate greetings, message examples, system prompt, post-history instructions, lorebook entry content, names, comments, and keys:

  • ChatML / Llama / Gemma / Mistral / Qwen / DeepSeek control tokens: <|system|>, <|user|>, <|assistant|>, <|im_start|>, <|im_end|>, plus a catch-all <|...|> wildcard.
  • [INST] and [/INST] (Llama-2 instruct).
  • <s> and </s> (BOS / EOS).
  • Tacita's own <think> and <tool> markers.

No redaction breadcrumb is left, by design — a sophisticated attacker would otherwise probe the filter.

Caps

FieldCap
name / nickname / creator80 chars
character_version32 chars
description16 KB
personality / scenario / post-history instructions / creator_notes4 KB
first_mes / each alternate_greeting8 KB
mes_example16 KB
system_prompt (instructions)8 KB
alternate_greetings count32
tags count64 (each ≤ 32 chars)
lorebook entries256
lorebook keys / secondary_keys32 per entry, 80 chars each
lorebook entry content4 KB
Avatar PNG4 MB
CHARX asset8 MB each, 32 total
CHARX decompressed total32 MB (zip-bomb guard)

Truncated text gets a visible inline marker. A typed advisory per truncation is pushed into the import preview so the user can see what was cut.

Macros

Tacita resolves the standard {{char}}, {{Char}}, {{CHAR}}, {{char_name}} macros to the persona's name (with Assistant as a fallback when empty), and the corresponding {{user}} variants to the user's display name. {{trim}}, {{newline}}, and {{noop}} behave as the convention specifies; unknown macros render verbatim without raising.

Tacita additionally supports {{random:a,b,c}}, {{roll:NdM±K}}, {{date}}, {{time}}, and {{idle_duration}}. Stable-prefix slots (description, personality, scenario, system prompt, message examples, post-history instructions) freeze these to deterministic values so the cached prompt prefix stays byte-stable across turns; resolve-now slots (first message, alternate greetings, lorebook content) get a live value each turn.

Privacy posture

  • No field destruction. Even unknown top-level data.* keys are preserved verbatim under a synthetic extensions.__tacita_unknown_data bucket.
  • Avatars live in the BlobStore. Content-addressed, encrypted under the vault key, decrypted lazily through BlobImageProvider for previews and persona avatars. Never on disk in cleartext.
  • Hidden extensions. Voice / TTS namespaces (top-level tts, voice, coqui, elevenlabs, plus their nested namespaces) are moved into a hidden bucket so the regular UI does not surface them. They are never destroyed and round-trip on export.
  • secureDelete the source. Opt-in (default ON) toggle in the import preview footer overwrites the source PNG / CHARX with random bytes via the same secureDelete primitive used to wipe vaults, then unlinks. Recovery is impossible.
  • Source URLs render as inert text. The CCv3 source[] array is preserved on the persona but is never linkified, never tap-to-open, and never copy-to-clipboard. Tacita does not facilitate navigation to character-card providers; it imports files the user already has.

Round-trip

Tacita losslessly round-trips every documented spec field plus the full extensions map (visible + hidden buckets). Each CharacterAsset retains its original URI so a future export rebuilds the CHARX layout exactly. Spec violations (missing name, unparseable JSON, unknown top-level format) raise a typed error rather than producing a partial persona — the user gets a clear failure, not a silently mangled card.


Related: the privacy architecture page for the encryption stack the BlobStore inherits, the glossary for term-level definitions, and the FAQ for short answers to the most common card questions.