Mikkeli d00af71a91 Prune card exports whose PNG is gone
Deleting a character in the UI left _text/cards/<Name>.json behind, describing a
card that no longer exists. A stale derived file is worse than a missing one
because it reads as current -- which is the exact reason the pre-commit hook
regenerates _text/ at all, so the script was failing its own premise.

Found by testing a deletion rather than reading the code. The write path was
obviously correct; the delete path had simply never been written.
2026-08-13 18:03:31 +09:00

SillyTavern world artifacts

Personas, lorebooks, character cards and presets for the SillyTavern instance on halogen — versioned in place, in SillyTavern's own live data directory.

Tracked What
worlds/ lorebooks / World Info — already JSON, tracked as-is
characters/ character cards, as SillyTavern stores them (PNG)
_text/cards/ the same cards extracted to readable JSON — this is the diff worth reading
_text/personas.json personas, lifted out of settings.json
QuickReplies/, OpenAI Settings/, User Avatars/ JSON presets and persona images
./st-export.py && git add -A && git commit    # after editing anything in the UI

data/ is a bind mount into the SillyTavern container. A symlink from data/default-user/worlds to a host path outside that mount resolves inside the container, where the target does not exist — so it would be a broken link to SillyTavern while looking correct on the host. Bind-mounting each artifact directory would work, at the cost of a mount entry per directory. Tracking in place avoids the whole class of problem.

⚠ The .gitignore is an ALLOWLIST, and that is load-bearing

It denies everything and re-includes named paths. This directory is SillyTavern's live user data, not a curated export: chat logs, caches, thumbnails, and whatever a future version decides to write here all land in it.

secrets.json is the reason. It is empty today, because API keys go through LiteLLM with a placeholder — and it arms itself the moment a key is typed into the UI. This repo is public. Under a denylist that file becomes trackable the first time someone adds a path and forgets it; under an allowlist it is invisible until a human names it.

Also untracked on purpose: settings.json (48 KB of UI state, rewritten on every change — personas are extracted from it instead), chats/ and group chats/ (the sessions themselves), and the 85 stock instruct/context/sysprompt templates, which ship with the app and are inert in Chat Completion mode.

⚠ Cards are PNGs with the JSON inside them

A character card is the PNG spec used as a container: v2 stores base64 JSON in a chara tEXt chunk, v3 in ccv3. So the PNG is what SillyTavern loads, and _text/cards/*.json is what a human reviews. st-export.py regenerates the latter with sort_keys, so re-exporting an unchanged card is a no-op diff.

The export is one-way. Editing _text/ does not change what SillyTavern sees. Re-embedding JSON into a PNG is a real operation but one where a mistake corrupts authored content, so it is deliberately not done implicitly. Edit in the UI, run the script, commit.

The SillyTavern deployment itself — compose.yaml, network posture, the heretic-27b constraints — lives in /home/mikkeli/docker/sillytavern/ and is documented in the halogen roadmap's Phase 16. ⚠ heretic-27b gets no tools, ever: it is an uncensored model, and combining that with execution is the one configuration that turns a writing tool into an incident.

S
Description
Personas, lorebooks and character cards for the halogen SillyTavern instance — versioned in place in its live data directory.
Readme 1.3 MiB
Languages
Python 100%