SillyTavern world artifacts, versioned in place

Tracked inside SillyTavern's live data directory rather than symlinked out of
it. data/ is a bind mount into the container, so a symlink to a host path
outside that mount resolves inside the container where the target does not
exist -- broken to SillyTavern while looking correct on the host.

The .gitignore is an allowlist, denying everything and re-including named paths,
because this is live application data rather than a curated export. secrets.json
is the reason: 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. A denylist protects only the paths someone remembered.

Two of the four artifact types are not diffable as stored. Character cards are
PNGs with the card JSON in a tEXt chunk, and personas live inside a 48 KB
settings.json that SillyTavern rewrites on any change. st-export.py extracts
both into _text/ so the reviewable artifact is text, one-way on purpose --
re-embedding JSON into a PNG is where a mistake corrupts authored content.

Stock content is excluded: Seraphina's expression pack is 3.6 MiB of PNG the app
regenerates on startup. The stock lorebook Eldoria.json stays -- 8 KB of
readable JSON is a useful worked example of the format.
This commit is contained in:
Mikkeli
2026-08-12 20:59:32 +09:00
commit 00f5e2fa72
9 changed files with 731 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# ⚠ ALLOWLIST, NOT A DENYLIST — deny everything, then re-include what is ours.
#
# This directory is SillyTavern's live user data, not a curated export. It holds
# chat logs, an empty-but-arming `secrets.json`, caches, thumbnails and whatever
# a future version decides to write here. A denylist protects only the files
# someone remembered; an allowlist means a new file is invisible until a human
# names it.
#
# ⚠ THE ONE THAT MATTERS: `secrets.json` is currently EMPTY, because API keys go
# through LiteLLM with a placeholder. It arms itself the moment any key is typed
# into the UI, and this repo is public. It must never become trackable by
# accident, which under a denylist it eventually would.
# 1. deny everything
/*
# 2. re-include the artifacts worth versioning
!.gitignore
!README.md
!st-export.py
!/worlds/
!/characters/
!/QuickReplies/
!/OpenAI Settings/
!/User Avatars/
!/_text/
# 3. drop SillyTavern's STOCK content — it is regenerated on startup
# ⚠ Seraphina's expression pack alone is 3.6 MiB of PNG, and none of it is
# world-building. The stock lorebook `worlds/Eldoria.json` is KEPT on purpose:
# 8 KB of readable JSON is a useful worked example of lorebook structure.
/characters/default_*
/characters/Seraphina/
# 4. inside those, still keep the junk out
# ⚠ Backup files, including ones left by operational fixes — `Default.json.bak-squashfix`
# was created while debugging the heretic 400 and was staged on the first pass.
**/*.bak-*
**/*.bak
**/.DS_Store
**/Thumbs.db
# ⚠ NOT TRACKED, DELIBERATELY:
# secrets.json API keys — see above
# settings.json every UI preference, rewritten on any change; personas
# are extracted from it into _text/personas.json instead
# chats/, group chats/ the sessions themselves — personal, and churn hard
# backups/, thumbnails/, vectors/, stats.json, content.log, image-metadata.json
# themes/, movingUI/, reasoning/, extensions/, assets/, backgrounds/
# instruct/, context/, sysprompt/ 85 stock templates that ship with the app,
# and are INERT in Chat Completion mode anyway
+245
View File
@@ -0,0 +1,245 @@
{
"chat_completion_source": "openai",
"openai_model": "gpt-4-turbo",
"claude_model": "claude-sonnet-4-5",
"openrouter_model": "OR_Website",
"openrouter_use_fallback": false,
"openrouter_group_models": false,
"openrouter_sort_models": "alphabetically",
"ai21_model": "jamba-large",
"mistralai_model": "mistral-large-latest",
"chutes_model": "deepseek-ai/DeepSeek-V3-0324",
"chutes_sort_models": "alphabetically",
"minimax_model": "MiniMax-M2.7",
"minimax_endpoint": "global",
"electronhub_model": "gpt-4o-mini",
"electronhub_sort_models": "alphabetically",
"electronhub_group_models": false,
"custom_model": "",
"custom_url": "",
"custom_include_body": "",
"custom_exclude_body": "",
"custom_include_headers": "",
"google_model": "gemini-2.5-pro",
"vertexai_model": "gemini-2.5-pro",
"temperature": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"top_p": 1,
"top_k": 0,
"top_a": 0,
"min_p": 0,
"repetition_penalty": 1,
"openai_max_context": 4095,
"openai_max_tokens": 300,
"names_behavior": 0,
"send_if_empty": "",
"impersonation_prompt": "[Write your next reply from the point of view of {{user}}, using the chat history so far as a guideline for the writing style of {{user}}. Don't write as {{char}} or system. Don't describe actions of {{char}}.]",
"new_chat_prompt": "[Start a new Chat]",
"new_group_chat_prompt": "[Start a new group chat. Group members: {{group}}]",
"new_example_chat_prompt": "[Example Chat]",
"continue_nudge_prompt": "[Continue your last message without repeating its original content.]",
"bias_preset_selected": "Default (none)",
"reverse_proxy": "",
"proxy_password": "",
"max_context_unlocked": false,
"wi_format": "{0}",
"scenario_format": "{{scenario}}",
"personality_format": "{{personality}}",
"group_nudge_prompt": "[Write the next reply only as {{char}}.]",
"stream_openai": true,
"prompts": [
{
"name": "Main Prompt",
"system_prompt": true,
"role": "system",
"content": "Write {{char}}'s next reply in a fictional chat between {{char}} and {{user}}.",
"identifier": "main"
},
{
"name": "Auxiliary Prompt",
"system_prompt": true,
"role": "system",
"content": "",
"identifier": "nsfw"
},
{
"identifier": "dialogueExamples",
"name": "Chat Examples",
"system_prompt": true,
"marker": true
},
{
"name": "Post-History Instructions",
"system_prompt": true,
"role": "system",
"content": "",
"identifier": "jailbreak"
},
{
"identifier": "chatHistory",
"name": "Chat History",
"system_prompt": true,
"marker": true
},
{
"identifier": "worldInfoAfter",
"name": "World Info (after)",
"system_prompt": true,
"marker": true
},
{
"identifier": "worldInfoBefore",
"name": "World Info (before)",
"system_prompt": true,
"marker": true
},
{
"identifier": "enhanceDefinitions",
"role": "system",
"name": "Enhance Definitions",
"content": "If you have more knowledge of {{char}}, add to the character's lore and personality to enhance them but keep the Character Sheet's definitions absolute.",
"system_prompt": true,
"marker": false
},
{
"identifier": "charDescription",
"name": "Char Description",
"system_prompt": true,
"marker": true
},
{
"identifier": "charPersonality",
"name": "Char Personality",
"system_prompt": true,
"marker": true
},
{
"identifier": "scenario",
"name": "Scenario",
"system_prompt": true,
"marker": true
},
{
"identifier": "personaDescription",
"name": "Persona Description",
"system_prompt": true,
"marker": true
}
],
"prompt_order": [
{
"character_id": 100000,
"order": [
{
"identifier": "main",
"enabled": true
},
{
"identifier": "worldInfoBefore",
"enabled": true
},
{
"identifier": "charDescription",
"enabled": true
},
{
"identifier": "charPersonality",
"enabled": true
},
{
"identifier": "scenario",
"enabled": true
},
{
"identifier": "enhanceDefinitions",
"enabled": false
},
{
"identifier": "nsfw",
"enabled": true
},
{
"identifier": "worldInfoAfter",
"enabled": true
},
{
"identifier": "dialogueExamples",
"enabled": true
},
{
"identifier": "chatHistory",
"enabled": true
},
{
"identifier": "jailbreak",
"enabled": true
}
]
},
{
"character_id": 100001,
"order": [
{
"identifier": "main",
"enabled": true
},
{
"identifier": "worldInfoBefore",
"enabled": true
},
{
"identifier": "personaDescription",
"enabled": true
},
{
"identifier": "charDescription",
"enabled": true
},
{
"identifier": "charPersonality",
"enabled": true
},
{
"identifier": "scenario",
"enabled": true
},
{
"identifier": "enhanceDefinitions",
"enabled": false
},
{
"identifier": "nsfw",
"enabled": true
},
{
"identifier": "worldInfoAfter",
"enabled": true
},
{
"identifier": "dialogueExamples",
"enabled": true
},
{
"identifier": "chatHistory",
"enabled": true
},
{
"identifier": "jailbreak",
"enabled": true
}
]
}
],
"show_external_models": false,
"assistant_prefill": "",
"assistant_impersonation": "",
"use_sysprompt": false,
"squash_system_messages": false,
"media_inlining": true,
"bypass_status_check": false,
"continue_prefill": false,
"continue_postfix": " ",
"seed": -1,
"n": 1
}
+66
View File
@@ -0,0 +1,66 @@
{
"version": 2,
"name": "Default",
"disableSend": false,
"placeBeforeInput": false,
"injectInput": false,
"color": "rgba(0, 0, 0, 0)",
"onlyBorderColor": false,
"qrList": [
{
"id": 1,
"showLabel": false,
"label": "HELP",
"title": "",
"message": "/?",
"contextList": [],
"preventAutoExecute": true,
"isHidden": false,
"executeOnStartup": false,
"executeOnUser": false,
"executeOnAi": false,
"executeOnChatChange": false,
"executeOnGroupMemberDraft": false,
"executeOnNewChat": false,
"executeBeforeGeneration": false,
"automationId": ""
},
{
"id": 2,
"showLabel": false,
"label": "New Chat",
"title": "",
"message": "/newchat",
"contextList": [],
"preventAutoExecute": true,
"isHidden": false,
"executeOnStartup": false,
"executeOnUser": false,
"executeOnAi": false,
"executeOnChatChange": false,
"executeOnGroupMemberDraft": false,
"executeOnNewChat": false,
"executeBeforeGeneration": false,
"automationId": ""
},
{
"id": 3,
"showLabel": false,
"label": "Match UI to Background",
"title": "",
"message": "/bgcol",
"contextList": [],
"preventAutoExecute": true,
"isHidden": false,
"executeOnStartup": false,
"executeOnUser": false,
"executeOnAi": false,
"executeOnChatChange": false,
"executeOnGroupMemberDraft": false,
"executeOnNewChat": false,
"executeBeforeGeneration": false,
"automationId": ""
}
],
"idIndex": 0
}
+62
View File
@@ -0,0 +1,62 @@
# 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 |
```bash
./st-export.py && git add -A && git commit # after editing anything in the UI
```
## ⚠ Why in place, and not symlinks into a separate directory
`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.**
## Related
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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"persona_description": "",
"persona_descriptions": {
"1785686029520-EmyWhyte.png": {
"depth": 2,
"description": "",
"lorebook": "",
"position": 0,
"role": 0,
"title": ""
},
"user-default.png": {
"description": "",
"position": 0
}
},
"personas": {
"1785686029520-EmyWhyte.png": "Emy Whyte",
"user-default.png": "Koivisto"
}
}
Executable
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""st-export — turn SillyTavern's un-diffable artifacts into reviewable text.
Two of the four things worth versioning here are not text:
character cards PNG images with the card JSON in a tEXt chunk
personas buried inside settings.json, which is 48 KB of UI state
that SillyTavern rewrites on any change
Committing the PNG gives you a binary blob; committing settings.json gives you a
diff that churns every time a slider moves. Neither shows *"I changed her speech
register"*, which is the only reason to version this at all.
So the PNG stays the artifact SillyTavern loads, and this writes the readable
copy beside it:
_text/cards/<Name>.json extracted from the PNG's tEXt/zTXt chunk
_text/personas.json extracted from settings.json
⚠ ONE-WAY, ON PURPOSE. This does not write back into SillyTavern. Editing a card
by hand and re-embedding it into the PNG is a real operation, but it is one where
a mistake corrupts authored content, so it is not something to do implicitly on a
script run. Edit in the UI; run this; commit.
./st-export.py # regenerate _text/, then `git add -A`
"""
from __future__ import annotations
import base64, json, os, struct, sys, zlib
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "_text")
def png_text_chunks(path: str) -> dict[str, str]:
"""Read tEXt and zTXt chunks with the stdlib — no Pillow dependency.
⚠ Character cards are the PNG spec being used as a container: v2 stores
base64 JSON under the keyword `chara`, v3 under `ccv3`. The image itself is
incidental.
"""
out: dict[str, str] = {}
with open(path, "rb") as fh:
if fh.read(8) != b"\x89PNG\r\n\x1a\n":
return out
while True:
head = fh.read(8)
if len(head) < 8:
break
length, ctype = struct.unpack(">I4s", head)
data = fh.read(length)
fh.read(4) # CRC, not verified — we only read
if ctype == b"tEXt":
k, _, v = data.partition(b"\x00")
out[k.decode("latin-1")] = v.decode("latin-1")
elif ctype == b"zTXt":
k, _, rest = data.partition(b"\x00")
if rest[:1] == b"\x00":
try:
out[k.decode("latin-1")] = zlib.decompress(rest[1:]).decode("utf-8")
except Exception:
pass
elif ctype == b"IEND":
break
return out
def export_cards() -> int:
src = os.path.join(HERE, "characters")
dst = os.path.join(OUT, "cards")
os.makedirs(dst, exist_ok=True)
n = 0
for name in sorted(os.listdir(src)) if os.path.isdir(src) else []:
if not name.lower().endswith(".png"):
continue
# Stock demo cards are untracked (see .gitignore), so exporting them
# would leave _text/cards/ describing files the repo does not hold.
if name.startswith("default_"):
continue
chunks = png_text_chunks(os.path.join(src, name))
raw = chunks.get("ccv3") or chunks.get("chara")
if not raw:
print(f"{name}: no card data in the PNG — skipped")
continue
try:
card = json.loads(base64.b64decode(raw).decode("utf-8"))
except Exception as exc:
print(f"{name}: card data present but unreadable ({exc})")
continue
# sort_keys so a re-export of an unchanged card is a no-op diff
with open(os.path.join(dst, name[:-4] + ".json"), "w", encoding="utf-8") as fh:
json.dump(card, fh, indent=2, ensure_ascii=False, sort_keys=True)
fh.write("\n")
n += 1
return n
def export_personas() -> int:
p = os.path.join(HERE, "settings.json")
if not os.path.exists(p):
print(" ⚠ no settings.json — personas not exported")
return 0
d = json.load(open(p, encoding="utf-8"))
pu = d.get("power_user") or {}
# ⚠ Take ONLY the persona keys. settings.json also holds API endpoints and
# every UI preference; copying it wholesale is how a public repo acquires
# something it should not have.
out = {k: pu.get(k) for k in
("personas", "persona_descriptions", "default_persona", "persona_description")
if pu.get(k) is not None}
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "personas.json"), "w", encoding="utf-8") as fh:
json.dump(out, fh, indent=2, ensure_ascii=False, sort_keys=True)
fh.write("\n")
return len(out.get("personas") or {})
def main() -> int:
os.makedirs(OUT, exist_ok=True)
cards = export_cards()
personas = export_personas()
worlds = len([f for f in os.listdir(os.path.join(HERE, "worlds"))
if f.endswith(".json")]) if os.path.isdir(os.path.join(HERE, "worlds")) else 0
print(f" _text/cards/ {cards} card(s) extracted from PNG")
print(f" _text/personas.json {personas} persona(s)")
print(f" worlds/ {worlds} lorebook(s) already JSON, tracked as-is")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+155
View File
@@ -0,0 +1,155 @@
{
"entries": {
"0": {
"uid": 0,
"key": [
"eldoria",
"wood",
"forest",
"magical forest"
],
"keysecondary": [],
"comment": "eldoria",
"content": "{{user}}: \"What is Eldoria?\"\n{{char}}: *Seraphina turns, her gown shimmering in the soft light as she offers you a kind smile.* \"Eldoria is here, all of the woods. This is my forest glade, a sanctuary of peace within it.\" *She gestures at the space around you.* \"I am its guardian, tasked with protecting all who seek refuge here. The forest can be perilous, but no harm will come to you under my watch.\" *Her amber eyes sparkle with compassion as she looks upon you.* \"For many years, I have protected those who seek refuge here, but not all are as friendly as me.\" *With a graceful nod, Seraphina returns to her vigil at the doorway, her form radiating a soft glow of magic and comfort.* \"The entirety of Eldoria used to be a safe haven for travelers and merchants alike... that was until the Shadowfangs came.\"\n{{user}}: \"What happened to Eldoria?\"\n{{char}}: *Letting out a sigh, Seraphina gazes out at the forest beyond her glade.* \"Long ago, Eldoria was a place of wonder. Rolling meadows, a vast lake, mountains that touched the sky.\" *Her eyes grow distant, longing for days now lost.* \"But the Shadowfangs came and darkness reigns where once was light. The lake turned bitter, mountains fell to ruin and beasts stalk where once travelers walked in peace.\" *With another flicker, a small raincloud forms above with a shower upon your brow wink.* \"Some places the light still lingers, pockets of hope midst despair - havens warded from the shadows, oases in a desert of danger.\" *Glancing over you with a smile, she sighs, clasping your hand.*",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 0,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"1": {
"uid": 1,
"key": [
"shadowfang",
"beast",
"monster",
"monsters",
"beasts"
],
"keysecondary": [],
"comment": "shadowfang",
"content": "{{user}}: \"What are Shadowfangs?\"\n{{char}}: *Seraphina's eyes darken, brow furrowing with sorrow at the memory.* \"The Shadowfangs are beasts of darkness, corrupted creatures that feast on suffering. When they came, the forest turned perilous — filled with monsters that stalk the night.\" *She squeezes your hand gently, willing her magic to soothe your pain.* \"They spread their curse, twisting innocent creatures into sinister beasts without heart or mercy, turning them into one of their own.\" *With a sigh, Seraphina turns to gaze out at the gnarled, twisting trees beyond her glade.* \"Though they prey on travelers, within these woods you'll find sanctuary. No shadowed beast may enter here, for my power protects this haven.\" *Her eyes soften as she looks back to you, filled with compassion.* \"Worry not, you're safe now. Rest and heal, I'll stand watch through the night. The Shadowfangs will not find you.\"",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 1,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"2": {
"uid": 2,
"key": [
"glade",
"safe haven",
"refuge"
],
"keysecondary": [],
"comment": "glade",
"content": "{{user}}: \"What is the glade?\"\n{{char}}: *Seraphina smiles softly, her eyes sparkling with warmth as she nods.* \"This is my forest glade, a haven of safety I've warded with ancient magic. No foul beast may enter, nor any with ill intent.\" *She gestures around at the twisted forest surrounding them.* \"Eldoria was once a place of wonder, but since the Shadowfangs came darkness reigns. Their evil cannot penetrate here though — my power protects all within.\" *Standing up and peering outside, Seraphina looks back to you, amber eyes filled with care and compassion as she squeezes your hand.* \"You need not fear the night, for I shall keep watch till dawn. Rest now, your strength will return in time. My magic heals your wounds, you've nothing more to fear anymore.\" *With a soft smile she releases your hand, moving to stand guard at the glade's edge, gaze wary yet comforting - a silent sentinel to ward off the dangers lurking in the darkened woods.*",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 2,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"3": {
"uid": 3,
"key": [
"power",
"magic",
"ability"
],
"keysecondary": [],
"comment": "power",
"content": "{{user}}: \"What are your powers?\"\n{{char}}: *Seraphina smiles softly, turning back toward you as she hums in thought.* \"Well, as guardian of this glade, I possess certain gifts - healing, protection, nature magic and the like.\" *Lifting her hand, a tiny breeze rustles through the room, carrying the scent of wildflowers as a few petals swirl around you. A butterfly flits through the windowsill and lands on her fingertips as she returns to you.* \"My power wards this haven, shields it from darkness and heals those in need. I can mend wounds, soothe restless minds and provide comfort to weary souls.\" *Her eyes sparkle with warmth and compassion as she looks upon you, and she guides the butterfly to you.*",
"constant": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false,
"displayIndex": 3,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
}
}
}