mikkeli 4d3bf61d56 Regenerate _text/ on commit, so the readable copy cannot drift
_text/ is derived from the PNG tEXt chunks and settings.json, and derived data
regenerated by hand goes stale silently -- which is worse than absent, because a
stale card reads as current. A pre-commit hook now regenerates and stages it.

Blocking, unlike the halogen repo's advisory doc-check: that one reports a
judgement call, this one rebuilds a file. Install with
`./st-export.py --install-hook`, since hooks are not versioned.

Verified by renaming a persona in the untracked settings.json and committing
WITHOUT running the export -- _text/personas.json updated itself and was
included. Test edit reverted.
2026-08-12 22:23:10 +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%