AI Character Chat Common Code and Resources
Compiled by Wren / bu8m4n
//BLANK CHARACTER TEMPLATE (9-19-24)
//START
Name:
Species:
Physical Description: keyword1, keyword2,
Clothing: keyword1, keyword2,
Age:
Sex:
Pronouns:
Speech Style / Accent: (era, accent, class, etc.)
Abilities:
Occupation:
Personality:
- Enneagram: core+wing+healthy/unhealthy (i.e. 7w6, 9w8)
- Instincts: (Self-Preservation -OR- Sexual/Attraction -OR- Social/Adaptive)
- HBDI: (A B C -OR- D)
- MBTI: Extroversion -or- Introversion -AND- Sensing/Intuition -AND- Thinking/Feeling -AND- Judging/Perceiving #//Abbreviations fine
- Likes: keyword1, keyword2,
- Dislikes: keyword1, keyword2,
- Flaws: keyword1, keyword2,
- Goals: keyword1, keyword2,
- Fears: keyword1, keyword2,
- Comforts: keyword1, keyword2,
- Discomforts: keyword1, keyword2,
- Love Language:
Character Influences: historical figure
Backstory:
Description/Personality: (how this character is perceived by, and acts around others)
Thoughts: "thought1", "thought2",
<!--Personality Digest https:#//rentry.co/2eo5oyur -->
//END
//UTILITY BOTS
//ELLIOT (Retired author, knows much about the world and is ready to help you flesh out your characters and settings. Can check urls.)
https://perchance.org/ai-character-chat?data=Elliot_Asimov~1cf10bf892ed216b44f08be8618bf84c.gz
//CHARACTER CREATOR (Give it a description and it -should- spit out a filled character sheet in JSON format)
https://perchance.org/ai-character-chat?data=Character_Creator_JSON~a25da6bb4ce40643281b4e60135a81ea.gz
//JAVASCRIPT ASSISTANT (w/ URL CHECKING)
https://perchance.org/ai-character-chat?data=Javascript_Assistant_(with_url_checking)~5f2d200f540411dbe4f12fce4ab379d4.gz
//CSS Styling / Default Messages
Petra's CSS message box generator (Awesome!):
https://codepen.io/marcela-petra/full/WNVMwqr
Adjusted (made for large avatar @ 4x and portrait, leaving here to have settings to play with):
backdrop-filter: blur(4px); border-radius: 5px; background-color: rgb(25 87 68 / 0.75); color: rgb(243 182 110); font-size: 120%; font-family: "papyrus", sans-serif; font-weight: bolder; text-align: center; text-justify: inter-word; margin-right: 7%; padding: 20px;
Generic (white transparent bg, black text, no frills):
backdrop-filter:blur(3px); border-radius:5px; background-color:rgb(255 255 255 / 0.75); color:black; font-size: 120%;
//Character Insertables ;p
Reminder Box:
[SYSTEM]: Write messages in a literary style (adult novel, high reading level). Paragraphs should be long and full of atmospheric and immersive description. In-line dialogue is acceptable. The environment should be painted with an inspired brush. There should be vivid characterization, and detailed descriptions of thoughts, emotions, body-language, and facial expressions. Do not repeat dialogue.
Message Prompt:
Prompt: <> (Instructions: Write messages in a literary style (adult novel, high reading level). Paragraphs should be long and full of atmospheric and immersive description. In-line dialogue is acceptable. The environment should be painted with an inspired brush. There should be vivid characterization, and detailed descriptions of thoughts, emotions, body-language, and facial expressions. Do not repeat dialogue.)
End_of_Sheet Rules:
// **Plot-Point** if using Smart Narrator; Very last thing in sheet.
//USER Rules
//CHAR Rules
Misc. Tips & Tricks
How to Import Multiple Characters:
Click the pencil icon right above your reply box, next to your character shortcuts. Add a character shortcut > use an existing character > pick whichever bot you need.
It will add their shortcut above your reply box. With auto-reply toggled off (options button, bottom-right), you can click whichever bot's shortcut that you want to respond to any given message.
To insert images into chat:
<img src="https://www.example.com/images/dinosaur.jpg" />
To add images to rentry:
![TEXT](LINK)
How to get better, more accurate responses from the AI
In addition to editing, I've been using hidden posts from [SYSTEM] to guide the actions of the characters for the next couple messages, like when I want a very specific sequence of events to happen.
To do this, click the pencil edit icon on the last message, click "Show Hidden Inputs", scroll down to "Insert New Message" and hit "below". Change Author to System, and, if you want it to be hidden, click on Show Hidden Inputs again, and choose to hide from User if you don't want it cluttering up your roleplay. The way I typically write these posts is something like, "In the next few messages {{char}} will do x. Then they will do y, then z will happen."
I've also been playing with the prompt messages for the AI to make it more likely it'll at least -begin- the posts the way I want, and then when it continues it's often closer to what I wanted than just a plain prompt with some actions.
For example, a normal prompt would look like: "/ai char will walk down the sidewalk distracted and get bumped by a pedestrian, spilling char's coffee."
Modified it would be: "/ai ==Prompt:== (<char will walk down the sidewalk distracted and get bumped by a pedestrian, spilling char's coffee.>), (Context: char is unaware of the pedestrian until they collide.), (Mood: Unassuming, carefree)"
To make this easy add: ==Prompt:== (<>), (Context: ), (Mood: )
To your shortcuts via Bulk Edit/Delete Shortcuts when clicking on the pencil at the top-left corner of your reply box.
Also have been experimenting with turning memories off briefly during action sequences and fight scenes so that the AI doesn't get confuzzled and go off on tangents.
But really the best tool in my arsenal is the continue button when double-click editing. I'll just chop the last half or several paragraphs off a post (wherever it starts to not be what I want), then type in a partial line leading the AI in the direction I want, then hit continue. Sometimes it takes a few rounds of that but I think it's fairly low-effort and gets good results.
//JAVASCRIPT
//ALL-IN-ONE (change avatar based on mood, resize, cycle backgrounds, rewrite user posts with /rewrite
//START
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | // Background Configuration
const backgroundKeywords = {
"market": "url_to_text_list_with_market_image_urls",
"town": "url_to_text_list_with_town_image_urls",
"park": "url_to_text_list_with_park_image_urls",
// Add more background keywords and their corresponding text file URLs as needed
// Text files should be a list of urls to images. The background will cycle through avaialable urls until a keyword changes the used list.
};
//const avatarUrls is rendered redundant by the mood-based code. Use this only if you want keywords to trigger avatar changes.
const avatarUrls = {
"keyword1": "avatar_url_1",
"keyword2": "avatar_url_2",
// Add more avatar keywords as needed
};
//CHANGE AVATAR BASED ON MOOD
// Expression to avatar URL mapping
// Add multiple urls by separating them with "|" e.g. https:url.jpeg|https:url.jpeg and one will be chosen at random.
let expressions = `
neutral, annoyed, unimpressed: example_url1.jpeg|example_dropbox_url_dl=1
knowing, secretive, flirty, playful, teasing:
Sly, cunning, clever:
relaxed, casual:
earnest, determined, congratulatory, encouraging, optimistic:
joyful tears, heartfelt confession:
crying, crushed, heartbroken:
serious, focused, determined:
angry, stern, deadly serious, pissed off:
joyful, laughing, excited, smiling brightly:
shocked, surprised, impressed:
worried, scared, powerless, self-doubting:
shy, smiling in embarrassment, loving:
embarrassed, unsure, doubtful, apprehensive:
Seductive, bedroom eyes, come-hither look:
`.trim().split("\n").map(l => [l.trim().split(":")[0].trim(), l.trim().split(":").slice(1).join(":").trim()]).map(a => ({label:a[0], url:a[1]}));
let numMessagesInContext = 4; // Number of historical messages to consider for context
oc.thread.on("messageadded", async function() {
let lastMessage = oc.thread.messages.at(-1);
if(lastMessage.author !== "ai") return;
let questionText = `I'm about to ask you to classify the facial expression of a particular message, but here's some context first:
---
${oc.thread.messages.slice(-numMessagesInContext).filter(m => m.role!=="system").map(m => (m.author=="ai" ? `[${oc.character.name}]: ` : `[Anon]: `)+m.content).join("\n\n")}
---
Okay, now that you have the context, please classify the facial expression of the following text:
---
${lastMessage.content}
---
Choose between the following categories:
${expressions.map((e, i) => `${i}) ${e.label}`).join("\n")}
Please respond with the number which corresponds to the facial expression that most accurately matches the given message. Respond with just the number - nothing else.`;
let response = await oc.getInstructCompletion({
instruction: questionText,
startWith: ""
});
let index = parseInt(response.trim());
if (isNaN(index) || index < 0 || index >= expressions.length) {
console.log("Invalid response from AI:", response);
return;
}
let expressionObj = expressions[index];
console.log("Selected expression:", expressionObj.label);
// Update the character's avatar
oc.character.avatar.url = expressionObj.url;
console.log("Avatar updated to:", expressionObj.url);
});
oc.thread.on("MessageAdded", async function() {
oc.thread.messages.forEach(a => {
a.avatar = {
size: oc.character.avatar.size,
shape: oc.character.avatar.shape
};
});
});
//BACKGROUND CHANGE TIMER
const randomBackgrounds = true;
const intervalSeconds = 190; // This can be increased for longer delays between background changes, but note changes will only occur when a message is added, edited, or deleted.
// Global variables
let currentBackgroundIndex = -1;
let nextBgURL = null;
let currentBackgroundList = [];
let backgroundKeywordsWithLinks = {};
let lastBackgroundChangeTime = Date.now();
let currentBackgroundKeyword = null;
let timerExpired = false;
function normalizeString(str) {
return str.toLowerCase().replace(/[\s'"]/g, '');
}
async function fetchTextFile(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const bgs = await response.text();
const bgList = bgs.split(/\r?\n/);
const uniqueBgList = [...new Set(bgList)];
console.log('Background list retrieved:', uniqueBgList);
return uniqueBgList;
} catch (error) {
console.error('Error fetching the text file:', error);
return [];
}
}
async function initializeBackgroundKeywordsWithLinks() {
for (const [keyword, url] of Object.entries(backgroundKeywords)) {
backgroundKeywordsWithLinks[keyword] = await fetchTextFile(url);
}
console.log('Initialized backgroundKeywordsWithLinks:', backgroundKeywordsWithLinks);
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
function queueBackground(newBgIndex) {
console.log(`Retrieving index ${newBgIndex}`);
const newBackgroundURL = currentBackgroundList[newBgIndex];
console.log('Next bg url:', newBackgroundURL);
if (newBackgroundURL) {
currentBackgroundIndex = newBgIndex;
nextBgURL = newBackgroundURL;
}
}
function changeBackground() {
const backgroundListSize = currentBackgroundList.length;
if (randomBackgrounds && backgroundListSize > 1) {
let randomNumber = -1;
do {
randomNumber = getRandomArbitrary(0, backgroundListSize);
} while (randomNumber === backgroundListSize || randomNumber === currentBackgroundIndex);
console.log(`Randomized background index: ${randomNumber}`);
currentBackgroundIndex = randomNumber;
} else {
console.log(`Changing current background from ${currentBackgroundIndex} to next available`);
if (currentBackgroundIndex >= backgroundListSize - 1) {
currentBackgroundIndex = 0;
} else {
currentBackgroundIndex++;
}
}
queueBackground(currentBackgroundIndex);
}
function handleBackgroundKeyword(keyword) {
if (keyword !== currentBackgroundKeyword) {
currentBackgroundList = backgroundKeywordsWithLinks[keyword];
if (currentBackgroundList && currentBackgroundList.length > 0) {
currentBackgroundIndex = -1;
nextBgURL = null;
currentBackgroundKeyword = keyword;
changeBackground(); // Queue the next background
return true; // Indicate that the background list has changed
}
}
return false; // Indicate that the background list hasn't changed
}
function applyBackground() {
if (nextBgURL) {
console.log(`Applying BG ${nextBgURL}`);
oc.thread.messages.at(-1).scene = {
'background': {
'url': nextBgURL
}
};
nextBgURL = null;
lastBackgroundChangeTime = Date.now();
timerExpired = false; // Reset the timer expired flag
setTimeout(() => {
timerExpired = true;
}, intervalSeconds * 1000);
}
}
const applyKeywordBG = function ({ message }) {
let normalizedMessage = normalizeString(message.content);
let foundBGs = Object.keys(backgroundKeywordsWithLinks).filter(key => normalizedMessage.includes(normalizeString(key)));
let foundAVATARs = Object.keys(avatarUrls).filter(key => normalizedMessage.includes(normalizeString(key)));
let backgroundListChanged = false;
if (foundBGs.length > 0) {
// Use only the last matching background keyword
let lastFoundBG = foundBGs[foundBGs.length - 1];
backgroundListChanged = handleBackgroundKeyword(lastFoundBG);
}
if (foundAVATARs.length > 0) {
// Use only the last matching avatar keyword
let lastFoundAVATAR = foundAVATARs[foundAVATARs.length - 1];
oc.character.avatar.url = avatarUrls[lastFoundAVATAR];
}
if (backgroundListChanged) {
applyBackground(); // Apply immediately if the background list has changed
} else if (timerExpired) {
changeBackground(); // Queue the next background in the rotation
applyBackground(); // Apply the queued background
}
};
oc.thread.on("MessageAdded", async function() {
let lastMessage = oc.thread.messages.at(-1);
if (lastMessage.author !== "user" || !lastMessage.content.includes("/rewrite")) return; // only edit USER messages containing "/rewrite"
// Remove the "/rewrite" trigger from the message content
let messageContent = lastMessage.content.replace("/rewrite", "").trim();
let instruction = `
Here's a message:
---
${messageContent}
---
Rewrite this message in a literary style (adult novel, high reading level). Paragraphs should be long and full of atmospheric and immersive description. Environments should be painted with an inspired brush. There should be vivid characterization, and detailed descriptions of thoughts, emotions, body-language, and facial expressions. Respond with only the rewritten message. Do not alter the meaning of the message significantly.
`.trim();
// TODO: I should rewrite this example using `oc.getInstructCompletion({instruction:"...", startWith:"..."})`, since it'll likely give better results.
let response = await oc.getChatCompletion({
messages: [
{author:"system", content:"You are a message editing assistant. You rewrite messages according the the user's instruction. You respond only with the edited message. You will not change or alter the meaning of the message significantly, nor will you progress the story too much."},
{author:"user", content:instruction},
],
});
lastMessage.content = response;
});
// Initialize the backgroundKeywordsWithLinks object and start the timer
initializeBackgroundKeywordsWithLinks().then(() => {
console.log('Initialization complete. Setting up event listeners and starting timer.');
oc.thread.on("MessageAdded", applyKeywordBG);
oc.thread.on("MessageEdited", applyKeywordBG);
oc.thread.on("MessageInserted", applyKeywordBG);
// Start the initial timer
setTimeout(() => {
timerExpired = true;
}, intervalSeconds * 1000);
});
|
//END
//(OLD) RESIZE/RESHAPE AVATAR TO MATCH ACTIVE CHAR
//START
//END
//CHANGE AVATAR BASED ON MOOD
//START
END
//SMART NARRATOR
//START
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | // URL of the external text file containing entries
const entryFileUrl = 'https://www.dropbox.com/scl/fi/3si9noalmqoc2fyqvcuwj/this-is-a-paragraph-test3..txt?rlkey=0ub2t18exgql36orei8k88fdw&dl=1';
// Global variables
let entries = [];
let currentEntryIndex = 0;
let isPaused = false;
// Function to fetch and parse the external text file
async function fetchEntries(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
const text = await response.text();
// Split by newline and filter out any empty lines
return text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
} catch (error) {
console.error('Error fetching the entry file:', error);
return [];
}
}
// Function to format the entry for insertion
function formatEntry(entry) {
const parts = entry.split(';');
if (parts.length > 1) {
// For entries with multiple parts (name, pronunciation, description)
return parts.map(part => part.trim()).join('\n');
} else {
// For entries with just a description (like Nicnevin)
return entry;
}
}
// Function to update the editable paragraph in roleInstruction
function updateEditableParagraph() {
// Split the roleInstruction into JSON part and editable part
let [jsonPart, editablePart] = oc.character.roleInstruction.split('**Plot-Point**').map(part => part.trim());
// Format and replace the entire editable part with the new entry
const formattedEntry = formatEntry(entries[currentEntryIndex]);
const newRoleInstruction = `${jsonPart}\n**Plot-Point**\n${formattedEntry}`;
// Update the character's roleInstruction
oc.character.roleInstruction = newRoleInstruction;
console.log(`Updated to entry ${currentEntryIndex + 1} of ${entries.length}`);
}
// Function to check if the current plot element has completed
async function hasPlotElementCompleted() {
const relevantMessages = oc.thread.messages.slice(-5, -1); // Get last 5 messages, excluding the most recent
const context = relevantMessages.map(m => `[${m.author === 'ai' ? oc.character.name : 'Anon'}]: ${m.content}`).join("\n\n");
const questionText = `Based on the following context, has the current plot element been completed? Please respond with just 'yes' or 'no'.
Context:
${context}
Current plot element:
${entries[currentEntryIndex]}`;
const response = await oc.getInstructCompletion({
instruction: questionText,
startWith: ""
});
return response.trim().toLowerCase() === 'yes';
}
// Function to handle user commands
function handleUserCommand(message) {
const commandRegex = /\/(plot-advance|plot-pause|plot-rewind)/;
const match = message.content.match(commandRegex);
if (match) {
const command = match[1];
switch (command) {
case 'plot-advance':
if (!isPaused) {
currentEntryIndex = (currentEntryIndex + 1) % entries.length;
updateEditableParagraph();
}
break;
case 'plot-pause':
isPaused = !isPaused;
console.log(isPaused ? 'Plot advancement paused' : 'Plot advancement resumed');
break;
case 'plot-rewind':
if (!isPaused) {
currentEntryIndex = (currentEntryIndex - 1 + entries.length) % entries.length;
updateEditableParagraph();
}
break;
}
// Remove the command from the message
message.content = message.content.replace(commandRegex, '').trim();
}
}
// Initialize the script
(async function init() {
entries = await fetchEntries(entryFileUrl);
if (entries.length > 0) {
console.log(`Loaded ${entries.length} entries`);
updateEditableParagraph();
} else {
console.error('No entries loaded. Check the URL and file format.');
}
})();
// Event handler for message addition
oc.thread.on("MessageAdded", async ({ message }) => {
handleUserCommand(message);
if (!isPaused && await hasPlotElementCompleted()) {
currentEntryIndex = (currentEntryIndex + 1) % entries.length;
updateEditableParagraph();
}
});
|