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.namename required, capped 80 chars data.descriptiondescription capped 16 KB data.personalitypersonality separate from description data.scenarioscenario per-chat scenario still wins data.first_mesfirstMessage capped 8 KB data.alternate_greetings[]alternateGreetings up to 32 data.mes_examplemessageExample <START> separators preserved data.creator_notescreatorNotes capped 4 KB data.creatorcreator data.character_versioncharacterVersion opaque string data.tags[]tags up to 64, each ≤ 32 chars data.system_promptinstructions capped 8 KB data.post_history_instructionspostHistoryInstructions separate data.character_bookcharacterBook full lorebook (see below) data.nickname (v3)nickname data.source[] (v3)sourceUrls rendered as inert text — never opened on tap data.group_only_greetings[] (v3)groupOnlyGreetings reserved for group chat data.creation_date (v3)creationDateUnix data.modification_date (v3)modificationDateUnix data.creator_notes_multilingual (v3)creatorNotesMultilingual data.assets[] (v3)assets bytes go through encrypted BlobStore data.extensionsextensions + hiddenExtensions voice 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.
- Always include
enabled && constant entries. -
For each non-constant
enabled entry, scan the joined
messages for any of keys (case-insensitive unless
caseSensitive).
-
If the entry is
selective, also require any of
secondaryKeys in the same window.
-
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.
- Substitute
{{char}} and {{user}} macros. -
Sort by
priority descending (null → 100), then
insertion_order ascending.
-
Greedy budget fill up to
tokenBudget × 4 chars
(default ~2000 chars). Atomic — entries are never partially
truncated.
-
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
Field Cap name / nickname / creator 80 chars character_version 32 chars description 16 KB personality / scenario / post-history instructions / creator_notes 4 KB first_mes / each alternate_greeting 8 KB mes_example 16 KB system_prompt (instructions) 8 KB alternate_greetings count 32 tags count 64 (each ≤ 32 chars) lorebook entries 256 lorebook keys / secondary_keys 32 per entry, 80 chars each lorebook entry content 4 KB Avatar PNG 4 MB CHARX asset 8 MB each, 32 total CHARX decompressed total 32 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.