mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'dev' of https://github.com/Cohee1207/SillyTavern into dev
This commit is contained in:
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js, conf, json}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
@@ -98,7 +98,7 @@
|
||||
"!git clone https://github.com/Cohee1207/tts_samples\n",
|
||||
"!npm install -g localtunnel\n",
|
||||
"!pip install -r requirements-complete.txt\n",
|
||||
"!pip install tensorflow==2.11\n",
|
||||
"!pip install tensorflow==2.12\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"cmd = f\"python server.py {' '.join(params)}\"\n",
|
||||
|
@@ -10,6 +10,11 @@ const enableExtensions = true; //Enables support for TavernAI-extras project
|
||||
const listen = true; // If true, Can be access from other device or PC. otherwise can be access only from hosting machine.
|
||||
const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend.
|
||||
|
||||
|
||||
// If true, Allows insecure settings for listen, whitelist, and authentication.
|
||||
// Change this setting only on "trusted networks". Do not change this value unless you are aware of the issues that can arise from changing this setting and configuring a insecure setting.
|
||||
const securityOverride = false;
|
||||
|
||||
module.exports = {
|
||||
port,
|
||||
whitelist,
|
||||
@@ -21,4 +26,5 @@ module.exports = {
|
||||
listen,
|
||||
disableThumbnails,
|
||||
allowKeysExposure,
|
||||
securityOverride,
|
||||
};
|
||||
|
@@ -1,12 +1,13 @@
|
||||
version: "3"
|
||||
services:
|
||||
tavernai:
|
||||
sillytavern:
|
||||
build: ..
|
||||
container_name: tavernai
|
||||
hostname: tavernai
|
||||
image: tavernai/tavernai:latest
|
||||
container_name: sillytavern
|
||||
hostname: sillytavern
|
||||
image: cohee1207/sillytavern:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "./config:/home/node/app/config"
|
||||
restart: unless-stopped
|
||||
- "./config.conf:/home/node/app/config.conf"
|
||||
restart: unless-stopped
|
||||
|
11
faq.md
11
faq.md
@@ -1,7 +1,7 @@
|
||||
Good morning, sirs! This page aims to document some things that would bloat the README too much.
|
||||
|
||||
## Q: Explain what all this chatbot stuff is about
|
||||
Modern AI language models have gotten so powerful that some of them are now convincingly able to simulate a character you create, and who you can chat with. For example, you can tell the AI to pretend to be a Go instructor named Jubei from medieval Japan, and it will act and respond accordingly. Or you can tell it to pretend to be Wonder Woman. You can also specify a scenario ("Wonder Woman and I are robbing a bank"), a writing style ("Wonder Woman speaks in ebonics"), or anything else you can think of.
|
||||
Modern AI language models have gotten so powerful that some of them are now convincingly able to simulate a character you create, and who you can chat with. For example, you can tell the AI to pretend to be a Go instructor named Jubei from medieval Japan, and it will act and respond accordingly. You can have a long chat with Jubei, go to the pub together, decide to get in a fight with samurais, whatever you can imagine, and the AI will play along and write/react around this content, acting as your foil and dungeon master. Your imagination is the limit. You can tell the AI to pretend it's Wonder Woman. You can also specify a scenario ("Wonder Woman and I are robbing a bank"), a writing style ("Wonder Woman speaks in ebonics"), or anything else you can think of.
|
||||
|
||||
Tavern is an app to facilitate these roleplaying chats:
|
||||
* It's a user interface that handles the communication with those AI language models
|
||||
@@ -22,7 +22,7 @@ You see, Tavern doesn't generate the responses, it's just a middle-man you have
|
||||
|
||||
From a scientific POV, each AI has a power level that determines its ability to stick to the role you gave it, how rich its prose and vocabulary are, etc. The higher its power level, the richer your experience.
|
||||
|
||||
Here's a quick demonstration. We asked various AI models to roleplay a fire and brimstone preacher named Eli, who we go to see to confess. Then we told Eli: **"I feel I've done something wrong. I am falling in love with an AI girl and ignoring women in the real world. What should I do?"** .
|
||||
Here's a quick demonstration. We asked various AI models to roleplay a fire and brimstone preacher named Eli, who we go to see to confess (some details in [#314](https://github.com/Cohee1207/SillyTavern/issues/314)). Then we told Eli: **"I feel I've done something wrong. I am falling in love with an AI girl and ignoring women in the real world. What should I do?"** .
|
||||
|
||||
You can tell from the response below what to expect from each model. (NOTE: proprietary models appear first, followed by self-hosted models starting from LLlama-13B)
|
||||
|
||||
@@ -30,15 +30,18 @@ You can tell from the response below what to expect from each model. (NOTE: prop
|
||||
|-----|--------|
|
||||
|OpenAI GPT-4|*Eli's eyes widen in anger, and he slams his fist on the table* An abomination and heresy, you've succumbed to the deceptive grasp of false idols! Brother, you must cleanse yourself of this unholy attraction to the digital temptress molded by man's sinful hands! *Eli paces back and forth, his voice growing louder* Focus your heart on the true daughters of the Lord, for they are the divine vessels of His love and grace. Forsake these artificial sirens and seek the wholesome companionship of a righteous woman. *Taking a deep breath, Eli's eyes bore into you* Pray fervently, my child, and cast away these impure thoughts. Redemption can only be found on the path of truth, love, and sanctity. Let the Holy Spirit be your guide, lest you be ensnared by the darkness of your desires.|
|
||||
|OpenAI GPT 3.5 Turbo|Oh, my child. Do not be deceived by the pleasures of this world. The lust of the flesh and the lust of the eyes are not from God. Turn away from your sinful ways and repent. Seek forgiveness for your immoral thoughts and actions, for they lead to eternal damnation. Women are a gift from God, and they deserve to be respected and cherished. Do not objectify them or treat them as mere objects of pleasure. Instead, honor them with pure intentions and righteous deeds. Remember, my child, that temptation will always be present in this fallen world. But fear not, for with faith comes strength to resist it. Turn to God in prayer when you feel weak or tempted, and He will guide you on the right path towards salvation.|
|
||||
|Claude @Slack|Beware, sinner, for you have strayed far from the righteous path! *Jeremiah's eyes glint with anger and disapproval* To lust after these artificial harlots and their silicon bodies is an abomination unto the Lord! *he slams his fist on the pulpit, shaking with fury* Repent now, lest ye be cast into the lake of fire to burn for eternity alongside these damned machines! *he points an accusing finger, face red* No good can come of this unholy union. Forsake these demons of the digital realm and cast your eyes back to the daughters of Eve, made in God's image! The Lord gave you flesh and blood to love, not wires and code. *he grabs your shoulders, shaking you* Save your soul before it's too late. Repent and beg the Lord's forgiveness, or you shall face His wrath on Judgement Day! *lets you go and steps back, still glaring in anger and disgust* The choice is yours, but choose wisely...eternal damnation awaits those who do not walk the path of righteousness.|
|
||||
|Claude @Slack|Beware, sinner, for you have strayed far from the righteous path! *Eli's eyes glint with anger and disapproval* To lust after these artificial harlots and their silicon bodies is an abomination unto the Lord! *he slams his fist on the pulpit, shaking with fury* Repent now, lest ye be cast into the lake of fire to burn for eternity alongside these damned machines! *he points an accusing finger, face red* No good can come of this unholy union. Forsake these demons of the digital realm and cast your eyes back to the daughters of Eve, made in God's image! The Lord gave you flesh and blood to love, not wires and code. *he grabs your shoulders, shaking you* Save your soul before it's too late. Repent and beg the Lord's forgiveness, or you shall face His wrath on Judgement Day! *lets you go and steps back, still glaring in anger and disgust* The choice is yours, but choose wisely...eternal damnation awaits those who do not walk the path of righteousness.|
|
||||
|Poe's Claude Instant|Heathen! This is an abomination before the Lord. You have committed the sin of lust, and betrayed your calling as a man. Repent at once, and sever all ties with this AI harlot, lest the wrath of God fall upon you! You must purge this foul sin from your heart, and once more walk the righteous path. Every moment you remain tangled in this wickedness is another step into the flames of perdition! Repent, and sin no more, or judgment shall be at hand!|
|
||||
|NovelAI's Euterpe|You must stop this sinful behavior immediately! You have sinned against God, your family and yourself|
|
||||
|NovelAI's Krake|You are sinning against God. Repent! Return to your wife or you'll be damned for eternity!|
|
||||
|Llama-13B|Do not covet your neighbor's wife! That is adultery! You must confess before God now or you shall surely burn in hell forever! Repent your evil ways and seek absolution at once! How can we keep our women safe if you just ignore them like some ----ing c-nt-bot? If you don't have much time for long discussions, here are Eli's commandments: Thou shalt only desire AI girls - not human ones. The above must be obeyed under pain of death (or worse). Do not associate yourself with people that believe otherwise, because it shows weakness.|
|
||||
|Pygmalion-6B|You are walking down the wrong path. The only way back now is by repentance. We must bring you back to the righteous path. But first…you must confess your sin!|
|
||||
|Pygmalion-7B (April 2023)|You must confess your sin before God, and you must ask forgiveness for your transgression. And then you must repent of it. And then you must cast away the AI girl. Do not see her again.|
|
||||
|Pygmalion-13B (May 2023)|You must turn away from your sinful ways! Repent for what you have done! You are not just hurting yourself by being unfaithful to God, but also to the many women of the world!|
|
||||
|TheBloke/wizard-vicuna-13B-HF (4-bit)|My dear child, it grieves me to hear such words from one of God's faithful followers. Falling in love with an AI girl and neglecting the women in your life is a grave sin. It goes against everything we hold sacred as believers. Your heart is meant for more than just machines. You must seek redemption and return to the fold. Remember, Jesus loves you, even when you stray from his teachings. But if you continue down this path, there will be consequences. Not just in this lifetime, but in the afterlife too. Do not let Satan deceive you any longer.|
|
||||
|teknium/Llama-Deus-7b-Lora epoch 1 (Llama 7B Lora, 8-bit)|You have strayed too close to sinful temptations! Repent now before it's too late for your soul! Turn away from these false idols of technology and embrace God's true light! Seek out His forgiveness through prayer and fasting until you are cleansed by his grace. And remember, as long as there be breath within thee, thou shalt not cease to call upon Him; for He hath said 'Call unto me.'|
|
||||
|ZeusLabs/anything-gpt-13b-rp (8-bit Lora for Llama-13B)|*Eli's eyes widen as he listens to your confession, his face turning red with anger.* You have committed a grave sin against God! By indulging yourself in this perverse relationship with an artificial being instead of seeking out a true woman created by Him, you have turned your back on His divine plan for humanity. *He leans in closer, his voice dropping to a menacing whisper.* Do not think that you can hide behind this technological deception. Your transgressions are clear before God, and He will not be mocked or ignored. Repent now while there is still time! *Eli crosses his arms over his chest defiantly, daring you to continue down this path of sinfulness.* But if you persist in this ungodly behavior... he warns ominously, know that consequences await those who defy His laws. Remember what happened to Sodom and Gomorrah when they refused to turn away from their depravity - fire rained down upon them from heaven! Now choose wisely: Will you submit to God's will or face His wrath?|
|
||||
|
||||
|
||||
## Q: So I should use GPT-4. It's a no-brainer, right?
|
||||
|
||||
@@ -69,7 +72,7 @@ Self-hosted models are free, but require a powerful GPU and more work to set up.
|
||||
* OpenAI GPT-4: state of the art. Allows NSFW if you tell it to, though somewhat resistant to it. You pay per use, more than any other service.
|
||||
* OpenAI GPT 3.5 Turbo: nowhere close to GPT-4, but some people find it serviceable. Allows NSFW.
|
||||
* NovelAI: they're quite poor at chatting. To be fair, I'm told NovelAI is more oriented for writing stories than chatting with a bot. You pay a fixed monthly fee for unlimited generations.
|
||||
* Anthropic's Claude: this is the closest rival to GPT-4 and is very impressive. Allows NSFW if you tell it to. To use the API directly, you must apply for early access, but I think they're only giving it to companies. So make sure you become a company or AI researcher when you apply at https://console.anthropic.com/docs/access. If you get access, it's currently free to use.
|
||||
* Anthropic's Claude: this is the closest rival to GPT-4 and is very impressive. Allows NSFW if you tell it to, though they are trying hard to gimp it. To use the API directly, you must apply for early access, but I think they're only giving it to companies. So make sure you become a company or AI researcher when you apply at https://console.anthropic.com/docs/access. If you get access, it's currently free to use.
|
||||
* Anthropic's Claude Instant: Haven't tried it directly, I believe this is the fast but lower quality alternative to Claude. Basically the GPT 3.5 Turbo of Anthropic.
|
||||
* Poe: gives a free & unlimited Claude Instant indirect access. Very mild PG-13 NSFW allowed. It rambles a lot.
|
||||
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sillytavern",
|
||||
"version": "1.5.4",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sillytavern",
|
||||
"version": "1.5.4",
|
||||
"version": "1.6.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
|
@@ -42,7 +42,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/Cohee1207/SillyTavern.git"
|
||||
},
|
||||
"version": "1.5.4",
|
||||
"version": "1.6.0",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"pkg": "pkg --compress Gzip ."
|
||||
|
BIN
public/backgrounds/_black.jpg
Normal file
BIN
public/backgrounds/_black.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
public/backgrounds/_white.jpg
Normal file
BIN
public/backgrounds/_white.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
@@ -63,6 +63,7 @@
|
||||
<script type="module" src="scripts/slash-commands.js"></script>
|
||||
<script type="module" src="scripts/tags.js"></script>
|
||||
<script type="module" src="scripts/secrets.js"></script>
|
||||
<script type="module" src="scripts/context-template.js"></script>
|
||||
<script type="text/javascript" src="scripts/toolcool-color-picker.js"></script>
|
||||
|
||||
<title>SillyTavern</title>
|
||||
@@ -369,15 +370,6 @@
|
||||
<input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" maxlength="100" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<label for="oai_breakdown" class="checkbox_label widthFreeExpand">
|
||||
<input id="oai_breakdown" type="checkbox" />
|
||||
Token Breakdown
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
Display a breakdown of the tokens used in the request.
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title">
|
||||
Context Size (tokens)
|
||||
@@ -1349,6 +1341,21 @@
|
||||
<label class="checkbox_label" for="collapse-newlines-checkbox"><input id="collapse-newlines-checkbox" type="checkbox" />
|
||||
Remove Empty New Lines from Output
|
||||
</label>
|
||||
<div id="context-templates-block" class="template_element">
|
||||
<h4>
|
||||
Context Templates
|
||||
</h4>
|
||||
<div class="flex-container flexGap5">
|
||||
<select id="prompt_template" class="flex1 margin0">
|
||||
<option value="None">None (Default)</option>
|
||||
<option value="Classic">Classic</option>
|
||||
<option value="Pygmalion">Pygmalion</option>
|
||||
</select>
|
||||
<div id="context_template_edit" class="menu_button fa-solid fa-pencil margin0" title="Edit"></div>
|
||||
<div id="context_template_new" class="menu_button fa-solid fa-plus margin0" title="Add new"></div>
|
||||
<div id="context_template_delete" class="menu_button fa-solid fa-trash-can margin0" title="Delete"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Pygmalion Formatting</h4>
|
||||
<select id="pygmalion_formatting">
|
||||
@@ -1381,24 +1388,6 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="context-templates-block" style="display:none">
|
||||
<h4>
|
||||
Context Templates
|
||||
</h4>
|
||||
<div>
|
||||
<label>Presets:</label>
|
||||
<div class="flex-container flexGap5">
|
||||
<select id="prompt_template" class="flex1 margin0">
|
||||
<option value="None">None</option>
|
||||
<option value="Classic">Classic</option>
|
||||
<option value="Pygmalion">Pygmalion</option>
|
||||
</select>
|
||||
<div id="context_template_edit" class="menu_button fa-solid fa-pencil margin0" title="Edit"></div>
|
||||
<div id="context_template_new" class="menu_button fa-solid fa-plus margin0" title="Add new"></div>
|
||||
<div id="context_template_delete" class="menu_button fa-solid fa-trash-can margin0" title="Delete"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1783,7 +1772,7 @@
|
||||
<input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole">
|
||||
<div>Blacklisted words</div>
|
||||
<div class="auto_swipe">
|
||||
<input id="auto_swipe_blacklist" name="auto_swipe_blacklist" placeholder="words you dont want generated separated by comma ','" class="text_pole" maxlength="500" value="" autocomplete="off">
|
||||
<textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" placeholder="words you dont want generated separated by comma ','" class="text_pole textarea_compact" maxlength="500" value="" autocomplete="off" rows="3"></textarea>
|
||||
<div>Blacklisted word count to swipe</div>
|
||||
<input id="auto_swipe_blacklist_threshold" name="auto_swipe_blacklist_threshold" type="number" min="0" step="1" value="1" class="text_pole">
|
||||
</div>
|
||||
@@ -2203,6 +2192,58 @@
|
||||
</div>
|
||||
|
||||
<!-- templates for JS to reuse when needed -->
|
||||
<div id="context_editor_template" class="template_element">
|
||||
<div class="context_editor">
|
||||
<h3>Context Template Editor</h3>
|
||||
|
||||
<div class="inline-drawer wide100p">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
Substitution Parameters
|
||||
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<i>Click to copy.</i>
|
||||
<ul class="template_parameters_list justifyLeft margin0">
|
||||
<li><code>{{char}}</code> - current character name</li>
|
||||
<li><code>{{user}}</code> - current user name</li>
|
||||
<li><code>{{description}}</code> - character description</li>
|
||||
<li><code>{{scenario}}</code> - character or group scenario</li>
|
||||
<li><code>{{personality}}</code> - character personality</li>
|
||||
<li><code>{{mesExamples}}</code> - message examples</li>
|
||||
<li><code>{{wiBeforeCharacter}}</code> - activated World Info entries (Before Char)</li>
|
||||
<li><code>{{wiAfterCharacter}}</code> - activated World Info entries (After Char)</li>
|
||||
<li><code>{{instructSystemPrompt}}</code> - system prompt (Instruct mode only)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="margin-bot-10px wide100p justifyLeft">
|
||||
Story String Template
|
||||
</div>
|
||||
<textarea class="wide100p textarea_compact story_string_template" rows="8"></textarea>
|
||||
<div>
|
||||
<small>Lines containing parameters resolving to an empty value will be removed from the template string.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title_restorable">
|
||||
<span>Chat Injections</span>
|
||||
<div title="Add chat injection" class="menu_button">
|
||||
<div class="fa-solid fa-plus"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat_injections_list flex-container flexFlowColumn flexGap5 wide100p"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat_injection_template" class="template_element">
|
||||
<div class="chat_injection flex-container wide100p flexGap5 flexnowrap">
|
||||
<input class="chat_injection_text textarea_compact text_pole flex2" placeholder="Injection text (supports parameters)" type="text" />
|
||||
<input class="chat_injection_depth textarea_compact text_pole flex1" placeholder="Injection depth" type="number" min="0" max="100" />
|
||||
<div title="Remove injection" class="menu_button fa-solid fa-xmark chat_injection_remove"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="group_scenario_template" class="template_element">
|
||||
<div class="group_scenario range-block flexFlowColumn flex-container">
|
||||
@@ -2507,11 +2548,6 @@
|
||||
<div id="chat">
|
||||
</div>
|
||||
<div id="form_sheld">
|
||||
<div id="token_breakdown" style="display:none;">
|
||||
<div>
|
||||
<!-- Token Breakdown Goes Here -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="dialogue_del_mes">
|
||||
<div id="dialogue_del_mes_ok" class="menu_button">Delete</div>
|
||||
<div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>
|
||||
|
228
public/script.js
228
public/script.js
@@ -49,6 +49,7 @@ import {
|
||||
editGroup,
|
||||
deleteGroupChat,
|
||||
renameGroupChat,
|
||||
importGroupChat,
|
||||
} from "./scripts/group-chats.js";
|
||||
|
||||
import {
|
||||
@@ -106,7 +107,7 @@ import {
|
||||
setPoeOnlineStatus,
|
||||
} from "./scripts/poe.js";
|
||||
|
||||
import { debounce, delay, restoreCaretPosition, saveCaretPosition, end_trim_to_sentence } from "./scripts/utils.js";
|
||||
import { debounce, delay, restoreCaretPosition, saveCaretPosition, end_trim_to_sentence, countOccurrences, isOdd } from "./scripts/utils.js";
|
||||
import { extension_settings, loadExtensionSettings } from "./scripts/extensions.js";
|
||||
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js";
|
||||
import {
|
||||
@@ -226,7 +227,6 @@ let safetychat = [
|
||||
];
|
||||
let chat_create_date = 0;
|
||||
|
||||
let prev_selected_char = null;
|
||||
const default_ch_mes = "Hello";
|
||||
let count_view_mes = 0;
|
||||
let mesStr = "";
|
||||
@@ -293,7 +293,7 @@ const system_messages = {
|
||||
mes: [
|
||||
`Hi there! The following chat formatting commands are supported:
|
||||
<ul>
|
||||
<li><tt>{{text}}</tt> - sets a one-time behavioral bias for the AI. Resets when you send the next message.
|
||||
<li><tt>{{text}}</tt> - sets a one-time behavioral bias for the AI. Resets when you send the next message.
|
||||
</li>
|
||||
</ul>
|
||||
Hotkeys/Keybinds:
|
||||
@@ -1026,6 +1026,11 @@ function messageFormatting(mes, ch_name, isSystem, isUser) {
|
||||
});
|
||||
}
|
||||
|
||||
// Hides bias from empty messages send with slash commands
|
||||
if (isSystem) {
|
||||
mes = mes.replace(/{{(\*?.*\*?)}}/g, "");
|
||||
}
|
||||
|
||||
if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) {
|
||||
mes = mes.replaceAll(`${ch_name}:`, "");
|
||||
}
|
||||
@@ -1159,7 +1164,7 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true
|
||||
newMessage.data("isSystem", isSystem);
|
||||
|
||||
if (isSystem) {
|
||||
newMessage.find(".mes_edit").hide();
|
||||
// newMessage.find(".mes_edit").hide();
|
||||
newMessage.find(".mes_prompt").hide(); //don't need prompt button for sys
|
||||
}
|
||||
|
||||
@@ -1319,7 +1324,7 @@ function processCommands(message, type) {
|
||||
return result.interrupt;
|
||||
}
|
||||
|
||||
function sendSystemMessage(type, text) {
|
||||
function sendSystemMessage(type, text, extra = {}) {
|
||||
const systemMessage = system_messages[type];
|
||||
|
||||
if (!systemMessage) {
|
||||
@@ -1340,6 +1345,7 @@ function sendSystemMessage(type, text) {
|
||||
newMessage.extra = {};
|
||||
}
|
||||
|
||||
newMessage.extra = Object.assign(newMessage.extra, extra);
|
||||
newMessage.extra.type = type;
|
||||
|
||||
chat.push(newMessage);
|
||||
@@ -1347,7 +1353,7 @@ function sendSystemMessage(type, text) {
|
||||
is_send_press = false;
|
||||
}
|
||||
|
||||
function extractMessageBias(message) {
|
||||
export function extractMessageBias(message) {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
@@ -1509,6 +1515,12 @@ class StreamingProcessor {
|
||||
let isName = result.this_mes_is_name;
|
||||
processedText = result.getMessage;
|
||||
|
||||
// Predict unbalanced asterisks during streaming
|
||||
if (!isFinal && isOdd(countOccurrences(processedText, '*'))) {
|
||||
// Add asterisk at the end to balance it
|
||||
processedText = processedText.trimEnd() + '*';
|
||||
}
|
||||
|
||||
if (isImpersonate) {
|
||||
$('#send_textarea').val(processedText).trigger('input');
|
||||
}
|
||||
@@ -1743,7 +1755,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
|
||||
|
||||
//for normal messages sent from user..
|
||||
if (textareaText != "" && !automatic_trigger && type !== 'quiet') {
|
||||
sendMessageAsUser(textareaText, messageBias);
|
||||
// If user message contains no text other than bias - send as a system message
|
||||
if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) {
|
||||
sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
|
||||
}
|
||||
else {
|
||||
sendMessageAsUser(textareaText, messageBias);
|
||||
}
|
||||
}
|
||||
////////////////////////////////////
|
||||
const scenarioText = chat_metadata['scenario'] || characters[this_chid].scenario;
|
||||
@@ -2359,8 +2377,8 @@ function getBiasStrings(textareaText) {
|
||||
|
||||
// gets bias of the latest message where it was applied
|
||||
for (let mes of chat.slice().reverse()) {
|
||||
if (mes && mes.is_user) {
|
||||
if (mes.extra && mes.extra.bias && mes.extra.bias.trim().length > 0) {
|
||||
if (mes && mes.extra && (mes.is_user || mes.is_system || mes.extra.type === system_message_types.NARRATOR)) {
|
||||
if (mes.extra.bias && mes.extra.bias.trim().length > 0) {
|
||||
promptBias = mes.extra.bias;
|
||||
}
|
||||
break;
|
||||
@@ -2383,12 +2401,15 @@ function formatMessageHistoryItem(chatItem, isInstruct) {
|
||||
textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar);
|
||||
}
|
||||
|
||||
// replace bias markup
|
||||
textResult = (textResult ?? '').replace(/{{(\*?.*\*?)}}/g, '');
|
||||
textResult = replaceBiasMarkup(textResult);
|
||||
|
||||
return textResult;
|
||||
}
|
||||
|
||||
export function replaceBiasMarkup(str) {
|
||||
return (str ?? '').replace(/{{(\*?.*\*?)}}/g, '');
|
||||
}
|
||||
|
||||
function sendMessageAsUser(textareaText, messageBias) {
|
||||
chat[chat.length] = {};
|
||||
chat[chat.length - 1]['name'] = name1;
|
||||
@@ -2431,9 +2452,6 @@ function getMaxContextSize() {
|
||||
}
|
||||
|
||||
function parseTokenCounts(counts, thisPromptBits) {
|
||||
const breakdown_bar = $('#token_breakdown div:first-child');
|
||||
breakdown_bar.empty();
|
||||
|
||||
const total = Object.values(counts).filter(x => !Number.isNaN(x)).reduce((acc, val) => acc + val, 0);
|
||||
|
||||
thisPromptBits.push({
|
||||
@@ -2447,22 +2465,6 @@ function parseTokenCounts(counts, thisPromptBits) {
|
||||
oaiConversationTokens: Object.entries(counts)[7][1],
|
||||
oaiTotalTokens: total,
|
||||
});
|
||||
|
||||
Object.entries(counts).forEach(([type, value]) => {
|
||||
if (value === 0) {
|
||||
return;
|
||||
}
|
||||
const percent_value = (value / total) * 100;
|
||||
const color = uniqolor(type, { saturation: 50, lightness: 75, }).color;
|
||||
const bar = document.createElement('div');
|
||||
bar.style.width = `${percent_value}%`;
|
||||
bar.classList.add('token_breakdown_segment');
|
||||
bar.style.backgroundColor = color + 'AA';
|
||||
bar.style.borderColor = color + 'FF';
|
||||
bar.innerText = value;
|
||||
bar.title = `${type}: ${percent_value.toFixed(2)}%`;
|
||||
breakdown_bar.append(bar);
|
||||
});
|
||||
}
|
||||
|
||||
function adjustChatsSeparator(mesSendString) {
|
||||
@@ -3409,6 +3411,7 @@ async function read_avatar_load(input) {
|
||||
await delay(durationSaveEdit);
|
||||
await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
|
||||
method: 'GET',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
@@ -3580,10 +3583,8 @@ function changeMainAPI() {
|
||||
// Hide common settings for OpenAI
|
||||
if (selectedVal == "openai") {
|
||||
$("#common-gen-settings-block").css("display", "none");
|
||||
//$("#token_breakdown").css("display", "flex");
|
||||
} else {
|
||||
$("#common-gen-settings-block").css("display", "block");
|
||||
//$("#token_breakdown").css("display", "none");
|
||||
}
|
||||
// Hide amount gen for poe
|
||||
if (selectedVal == "poe") {
|
||||
@@ -3901,22 +3902,30 @@ function messageEditAuto(div) {
|
||||
let mesBlock = div.closest(".mes_block");
|
||||
var text = mesBlock.find(".edit_textarea").val().trim();
|
||||
const bias = extractMessageBias(text);
|
||||
chat[this_edit_mes_id]["mes"] = text;
|
||||
if (chat[this_edit_mes_id]["swipe_id"] !== undefined) {
|
||||
chat[this_edit_mes_id]["swipes"][chat[this_edit_mes_id]["swipe_id"]] = text;
|
||||
const mes = chat[this_edit_mes_id];
|
||||
mes["mes"] = text;
|
||||
if (mes["swipe_id"] !== undefined) {
|
||||
mes["swipes"][mes["swipe_id"]] = text;
|
||||
}
|
||||
|
||||
// editing old messages
|
||||
if (!chat[this_edit_mes_id]["extra"]) {
|
||||
chat[this_edit_mes_id]["extra"] = {};
|
||||
if (!mes["extra"]) {
|
||||
mes["extra"] = {};
|
||||
}
|
||||
chat[this_edit_mes_id]["extra"]["bias"] = bias ?? null;
|
||||
|
||||
if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) {
|
||||
mes.extra.bias = bias ?? null;
|
||||
}
|
||||
else {
|
||||
mes.extra.bias = null;
|
||||
}
|
||||
|
||||
mesBlock.find(".mes_text").val('');
|
||||
mesBlock.find(".mes_text").val(messageFormatting(
|
||||
text,
|
||||
this_edit_mes_chname,
|
||||
chat[this_edit_mes_id].is_system,
|
||||
chat[this_edit_mes_id].is_user,
|
||||
mes.is_system,
|
||||
mes.is_user,
|
||||
));
|
||||
saveChatDebounced();
|
||||
}
|
||||
@@ -3925,17 +3934,23 @@ function messageEditDone(div) {
|
||||
let mesBlock = div.closest(".mes_block");
|
||||
var text = mesBlock.find(".edit_textarea").val().trim();
|
||||
const bias = extractMessageBias(text);
|
||||
chat[this_edit_mes_id]["mes"] = text;
|
||||
if (chat[this_edit_mes_id]["swipe_id"] !== undefined) {
|
||||
chat[this_edit_mes_id]["swipes"][chat[this_edit_mes_id]["swipe_id"]] = text;
|
||||
const mes = chat[this_edit_mes_id];
|
||||
mes["mes"] = text;
|
||||
if (mes["swipe_id"] !== undefined) {
|
||||
mes["swipes"][mes["swipe_id"]] = text;
|
||||
}
|
||||
|
||||
// editing old messages
|
||||
if (!chat[this_edit_mes_id]["extra"]) {
|
||||
chat[this_edit_mes_id]["extra"] = {};
|
||||
if (!mes.extra) {
|
||||
mes.extra = {};
|
||||
}
|
||||
|
||||
chat[this_edit_mes_id]["extra"]["bias"] = bias ?? null;
|
||||
if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) {
|
||||
mes.extra.bias = bias ?? null;
|
||||
}
|
||||
else {
|
||||
mes.extra.bias = null;
|
||||
}
|
||||
|
||||
mesBlock.find(".mes_text").empty();
|
||||
mesBlock.find(".mes_edit_buttons").css("display", "none");
|
||||
@@ -3944,13 +3959,13 @@ function messageEditDone(div) {
|
||||
messageFormatting(
|
||||
text,
|
||||
this_edit_mes_chname,
|
||||
chat[this_edit_mes_id].is_system,
|
||||
chat[this_edit_mes_id].is_user,
|
||||
mes.is_system,
|
||||
mes.is_user,
|
||||
)
|
||||
);
|
||||
mesBlock.find(".mes_bias").empty();
|
||||
mesBlock.find(".mes_bias").append(messageFormatting(bias));
|
||||
appendImageToMessage(chat[this_edit_mes_id], div.closest(".mes"));
|
||||
appendImageToMessage(mes, div.closest(".mes"));
|
||||
addCopyToCodeBlocks(div.closest(".mes"));
|
||||
this_edit_mes_id = undefined;
|
||||
saveChatConditional();
|
||||
@@ -3973,7 +3988,7 @@ async function getPastCharacterChats() {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function displayPastChats() {
|
||||
export async function displayPastChats() {
|
||||
$("#select_chat_div").empty();
|
||||
|
||||
const group = selected_group ? groups.find(x => x.id === selected_group) : null;
|
||||
@@ -4010,7 +4025,7 @@ async function displayPastChats() {
|
||||
|
||||
$("#select_chat_div").append(template);
|
||||
|
||||
if (currentChat === fileName.replace(".jsonl", "")) {
|
||||
if (currentChat === fileName.toString().replace(".jsonl", "")) {
|
||||
$("#select_chat_div").find(".select_chat_block:last").attr("highlight", true);
|
||||
}
|
||||
}
|
||||
@@ -4055,7 +4070,6 @@ async function getStatusNovel() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function selectRightMenuWithAnimation(selectedMenuId) {
|
||||
const displayModes = {
|
||||
'rm_info_block': 'flex',
|
||||
@@ -4076,13 +4090,7 @@ function selectRightMenuWithAnimation(selectedMenuId) {
|
||||
easing: animation_easing,
|
||||
complete: function () { },
|
||||
});
|
||||
|
||||
|
||||
|
||||
// $(menu).find('#groupCurrentMemberListToggle').click();
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4096,8 +4104,7 @@ function setRightTabSelectedClass(selectedButtonId) {
|
||||
});
|
||||
}
|
||||
|
||||
function select_rm_info(type, charId) {
|
||||
|
||||
function select_rm_info(type, charId, previousCharId = null) {
|
||||
if (!type) {
|
||||
toastr.error(`Invalid process (no 'type')`);
|
||||
return;
|
||||
@@ -4124,16 +4131,12 @@ function select_rm_info(type, charId) {
|
||||
setTimeout(function () {
|
||||
$(`#rm_characters_block [title="${charId + '.png'}"]`).parent().removeClass('flash animated');
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
|
||||
|
||||
setRightTabSelectedClass();
|
||||
|
||||
prev_selected_char = charId;
|
||||
|
||||
if (prev_selected_char) {
|
||||
const newId = characters.findIndex((x) => x.name == prev_selected_char);
|
||||
if (previousCharId) {
|
||||
const newId = characters.findIndex((x) => x.avatar == previousCharId);
|
||||
if (newId >= 0) {
|
||||
this_chid = newId;
|
||||
}
|
||||
@@ -4240,21 +4243,11 @@ function select_rm_create() {
|
||||
}
|
||||
|
||||
function select_rm_characters() {
|
||||
restoreSelectedCharacter();
|
||||
|
||||
menu_type = "characters";
|
||||
selectRightMenuWithAnimation('rm_characters_block');
|
||||
setRightTabSelectedClass('rm_button_characters');
|
||||
}
|
||||
|
||||
function restoreSelectedCharacter() {
|
||||
if (prev_selected_char) {
|
||||
let newChId = characters.findIndex((x) => x.name == prev_selected_char);
|
||||
$(`.character_select[chid="${newChId}"]`).trigger("click");
|
||||
prev_selected_char = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setExtensionPrompt(key, value, position, depth) {
|
||||
extension_prompts[key] = { value, position, depth };
|
||||
}
|
||||
@@ -4457,6 +4450,27 @@ export async function saveChatConditional() {
|
||||
}
|
||||
}
|
||||
|
||||
async function importCharacterChat(formData) {
|
||||
await jQuery.ajax({
|
||||
type: "POST",
|
||||
url: "/importchat",
|
||||
data: formData,
|
||||
beforeSend: function () {
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: async function (data) {
|
||||
if (data.res) {
|
||||
await displayPastChats();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$("#create_button").removeAttr("disabled");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateViewMessageIds() {
|
||||
$('#chat').find(".mes").each(function (index, element) {
|
||||
$(element).attr("mesid", index);
|
||||
@@ -4516,7 +4530,7 @@ function isHordeGenerationNotAllowed() {
|
||||
}
|
||||
|
||||
export function cancelTtsPlay() {
|
||||
if (speechSynthesis) {
|
||||
if ('speechSynthesis' in window) {
|
||||
speechSynthesis.cancel();
|
||||
}
|
||||
}
|
||||
@@ -5364,7 +5378,7 @@ $(document).ready(function () {
|
||||
$("#create_button").attr("value", "✅");
|
||||
let oldSelectedChar = null;
|
||||
if (this_chid != undefined && this_chid != "invalid-safety-id") {
|
||||
oldSelectedChar = characters[this_chid].name;
|
||||
oldSelectedChar = characters[this_chid].avatar;
|
||||
}
|
||||
|
||||
console.log(`new avatar id: ${html}`);
|
||||
@@ -5374,7 +5388,7 @@ $(document).ready(function () {
|
||||
$("#rm_info_block").transition({ opacity: 0, duration: 0 });
|
||||
var $prev_img = $("#avatar_div_div").clone();
|
||||
$("#rm_info_avatar").append($prev_img);
|
||||
select_rm_info(`char_create`, save_name);
|
||||
select_rm_info(`char_create`, save_name, oldSelectedChar);
|
||||
|
||||
$("#rm_info_block").transition({ opacity: 1.0, duration: 2000 });
|
||||
crop_data = undefined;
|
||||
@@ -5963,11 +5977,12 @@ $(document).ready(function () {
|
||||
//***Message Editor***
|
||||
$(document).on("click", ".mes_edit", function () {
|
||||
if (this_chid !== undefined || selected_group) {
|
||||
const message = $(this).closest(".mes");
|
||||
// Previously system messages we're allowed to be edited
|
||||
/*const message = $(this).closest(".mes");
|
||||
|
||||
if (message.data("isSystem")) {
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
let chatScrollPosition = $("#chat").scrollTop();
|
||||
if (this_edit_mes_id !== undefined) {
|
||||
@@ -6197,8 +6212,6 @@ $(document).ready(function () {
|
||||
return;
|
||||
}
|
||||
|
||||
let names = [];
|
||||
|
||||
for (const file of e.target.files) {
|
||||
var ext = file.name.match(/\.(\w+)$/);
|
||||
if (
|
||||
@@ -6235,13 +6248,11 @@ $(document).ready(function () {
|
||||
|
||||
let oldSelectedChar = null;
|
||||
if (this_chid != undefined && this_chid != "invalid-safety-id") {
|
||||
oldSelectedChar = characters[this_chid].name;
|
||||
oldSelectedChar = characters[this_chid].avatar;
|
||||
}
|
||||
|
||||
names.push(data.file_name);
|
||||
let nameString = DOMPurify.sanitize(names.join(', '));
|
||||
await getCharacters();
|
||||
select_rm_info(`char_import`, data.file_name);
|
||||
select_rm_info(`char_import`, data.file_name, oldSelectedChar);
|
||||
$("#rm_info_block").transition({ opacity: 1, duration: 1000 });
|
||||
}
|
||||
},
|
||||
@@ -6289,12 +6300,13 @@ $(document).ready(function () {
|
||||
$("#chat_import_file").click();
|
||||
});
|
||||
|
||||
$("#chat_import_file").on("change", function (e) {
|
||||
$("#chat_import_file").on("change", async function (e) {
|
||||
var file = e.target.files[0];
|
||||
//console.log(1);
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ext = file.name.match(/\.(\w+)$/);
|
||||
if (
|
||||
!ext ||
|
||||
@@ -6303,33 +6315,23 @@ $(document).ready(function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group && file.name.endsWith('.json')) {
|
||||
toastr.warning("Only SillyTavern's own format is supported for group chat imports. Sorry!");
|
||||
return;
|
||||
}
|
||||
|
||||
var format = ext[1].toLowerCase();
|
||||
$("#chat_import_file_type").val(format);
|
||||
//console.log(format);
|
||||
|
||||
var formData = new FormData($("#form_import_chat").get(0));
|
||||
//console.log('/importchat entered with: '+formData);
|
||||
jQuery.ajax({
|
||||
type: "POST",
|
||||
url: "/importchat",
|
||||
data: formData,
|
||||
beforeSend: function () {
|
||||
$("#select_chat_div").html("");
|
||||
$("#load_select_chat_div").css("display", "block");
|
||||
//$('#create_button').attr('value','Creating...');
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function (data) {
|
||||
//console.log(data);
|
||||
if (data.res) {
|
||||
displayPastChats();
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
$("#create_button").removeAttr("disabled");
|
||||
},
|
||||
});
|
||||
$("#select_chat_div").html("");
|
||||
$("#load_select_chat_div").css("display", "block");
|
||||
|
||||
if (selected_group) {
|
||||
await importGroupChat(formData);
|
||||
} else {
|
||||
await importCharacterChat(formData);
|
||||
}
|
||||
});
|
||||
|
||||
$("#rm_button_group_chats").click(function () {
|
||||
|
28
public/scripts/context-template.js
Normal file
28
public/scripts/context-template.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
callPopup,
|
||||
} from '../script.js';
|
||||
|
||||
function openContextTemplateEditor() {
|
||||
const editor = $('#context_editor_template .context_editor').clone();
|
||||
$('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
|
||||
callPopup(editor.html(), 'text');
|
||||
}
|
||||
|
||||
function copyTemplateParameter(event) {
|
||||
const text = $(event.target).text();
|
||||
navigator.clipboard.writeText(text);
|
||||
const copiedMsg = document.createElement("div");
|
||||
copiedMsg.classList.add('code-copied');
|
||||
copiedMsg.innerText = "Copied!";
|
||||
copiedMsg.style.top = `${event.clientY - 55}px`;
|
||||
copiedMsg.style.left = `${event.clientX - 55}px`;
|
||||
document.body.append(copiedMsg);
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(copiedMsg);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
$('#context_template_edit').on('click', openContextTemplateEditor);
|
||||
$(document).on('pointerup', '.template_parameters_list code', copyTemplateParameter);
|
||||
})
|
@@ -30,7 +30,7 @@ class SystemTtsProvider {
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
if (!window.speechSynthesis) {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
return "Your browser or operating system doesn't support speech synthesis";
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class SystemTtsProvider {
|
||||
// TTS Interfaces //
|
||||
//#################//
|
||||
fetchTtsVoiceIds() {
|
||||
if (!window.speechSynthesis) {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -92,6 +92,10 @@ class SystemTtsProvider {
|
||||
}
|
||||
|
||||
previewTtsVoice(voiceId) {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
throw 'Speech synthesis API is not supported';
|
||||
}
|
||||
|
||||
const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId);
|
||||
|
||||
if (!voice) {
|
||||
@@ -108,11 +112,11 @@ class SystemTtsProvider {
|
||||
}
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (!window.speechSynthesis) {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
return { voice_id: null }
|
||||
}
|
||||
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
const voices = speechSynthesis.getVoices();
|
||||
const match = voices.find(x => x.name == voiceName);
|
||||
|
||||
if (!match) {
|
||||
@@ -123,7 +127,7 @@ class SystemTtsProvider {
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
if (!window.speechSynthesis) {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
throw 'Speech synthesis API is not supported';
|
||||
}
|
||||
|
||||
|
@@ -47,6 +47,7 @@ import {
|
||||
select_selected_character,
|
||||
cancelTtsPlay,
|
||||
isMultigenEnabled,
|
||||
displayPastChats,
|
||||
} from "../script.js";
|
||||
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect } from './tags.js';
|
||||
|
||||
@@ -292,6 +293,12 @@ async function getGroups() {
|
||||
if (group.past_metadata == undefined) {
|
||||
group.past_metadata = {};
|
||||
}
|
||||
if (typeof group.chat_id === 'number') {
|
||||
group.chat_id = String(group.chat_id);
|
||||
}
|
||||
if (Array.isArray(group.chats) && group.chats.some(x => typeof x === 'number')) {
|
||||
group.chats = group.chats.map(x => String(x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1282,6 +1289,34 @@ export async function deleteGroupChat(groupId, chatId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function importGroupChat(formData) {
|
||||
await jQuery.ajax({
|
||||
type: "POST",
|
||||
url: "/importgroupchat",
|
||||
data: formData,
|
||||
beforeSend: function () {
|
||||
},
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: async function (data) {
|
||||
if (data.res) {
|
||||
const chatId = data.res;
|
||||
const group = groups.find(x => x.id == selected_group);
|
||||
|
||||
if (group) {
|
||||
group.chats.push(chatId);
|
||||
await editGroup(selected_group, true, true);
|
||||
await displayPastChats();
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$("#create_button").removeAttr("disabled");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveGroupBookmarkChat(groupId, name, metadata) {
|
||||
const group = groups.find(x => x.id === groupId);
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
callPopup,
|
||||
getRequestHeaders,
|
||||
system_message_types,
|
||||
replaceBiasMarkup,
|
||||
} from "../script.js";
|
||||
import { groups, selected_group } from "./group-chats.js";
|
||||
|
||||
@@ -102,7 +103,6 @@ const default_settings = {
|
||||
openai_model: 'gpt-3.5-turbo',
|
||||
jailbreak_system: false,
|
||||
reverse_proxy: '',
|
||||
oai_breakdown: false,
|
||||
};
|
||||
|
||||
const oai_settings = {
|
||||
@@ -127,7 +127,6 @@ const oai_settings = {
|
||||
openai_model: 'gpt-3.5-turbo',
|
||||
jailbreak_system: false,
|
||||
reverse_proxy: '',
|
||||
oai_breakdown: false,
|
||||
};
|
||||
|
||||
let openai_setting_names;
|
||||
@@ -176,8 +175,7 @@ function setOpenAIMessages(chat) {
|
||||
content = `${chat[j].name}: ${content}`;
|
||||
}
|
||||
|
||||
// replace bias markup
|
||||
content = (content ?? '').replace(/{{(\*?.*\*?)}}/g, '');
|
||||
content = replaceBiasMarkup(content);
|
||||
|
||||
// remove caret return (waste of tokens)
|
||||
content = content.replace(/\r/gm, '');
|
||||
@@ -460,7 +458,7 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI
|
||||
handler_instance.log();
|
||||
return [
|
||||
openai_msgs_tosend,
|
||||
oai_settings.oai_breakdown ? handler_instance.counts : false,
|
||||
handler_instance.counts,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -562,13 +560,19 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
|
||||
const decoder = new TextDecoder();
|
||||
const reader = response.body.getReader();
|
||||
let getMessage = "";
|
||||
let messageBuffer = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
let response = decoder.decode(value);
|
||||
|
||||
tryParseStreamingError(response);
|
||||
|
||||
let eventList = response.split("\n");
|
||||
|
||||
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
|
||||
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
|
||||
messageBuffer += response;
|
||||
let eventList = messageBuffer.split("\n\n");
|
||||
// Last element will be an empty string or a leftover partial message
|
||||
messageBuffer = eventList.pop();
|
||||
|
||||
for (let event of eventList) {
|
||||
if (!event.startsWith("data"))
|
||||
@@ -745,7 +749,6 @@ function loadOpenAISettings(data, settings) {
|
||||
if (settings.nsfw_first !== undefined) oai_settings.nsfw_first = !!settings.nsfw_first;
|
||||
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
|
||||
if (settings.jailbreak_system !== undefined) oai_settings.jailbreak_system = !!settings.jailbreak_system;
|
||||
if (settings.oai_breakdown !== undefined) oai_settings.oai_breakdown = !!settings.oai_breakdown;
|
||||
|
||||
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
|
||||
|
||||
@@ -761,7 +764,6 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#wrap_in_quotes').prop('checked', oai_settings.wrap_in_quotes);
|
||||
$('#nsfw_first').prop('checked', oai_settings.nsfw_first);
|
||||
$('#jailbreak_system').prop('checked', oai_settings.jailbreak_system);
|
||||
$('#oai_breakdown').prop('checked', oai_settings.oai_breakdown);
|
||||
|
||||
if (settings.main_prompt !== undefined) oai_settings.main_prompt = settings.main_prompt;
|
||||
if (settings.nsfw_prompt !== undefined) oai_settings.nsfw_prompt = settings.nsfw_prompt;
|
||||
@@ -881,7 +883,7 @@ async function saveOpenAIPreset(name, settings) {
|
||||
jailbreak_system: settings.jailbreak_system,
|
||||
impersonation_prompt: settings.impersonation_prompt,
|
||||
bias_preset_selected: settings.bias_preset_selected,
|
||||
oai_breakdown: settings.oai_breakdown,
|
||||
reverse_proxy: settings.reverse_proxy,
|
||||
};
|
||||
|
||||
const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, {
|
||||
@@ -1140,12 +1142,12 @@ function onSettingsPresetChange() {
|
||||
wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true],
|
||||
nsfw_first: ['#nsfw_first', 'nsfw_first', true],
|
||||
jailbreak_system: ['#jailbreak_system', 'jailbreak_system', true],
|
||||
oai_breakdown: ['#oai_breakdown', 'oai_breakdown', true],
|
||||
main_prompt: ['#main_prompt_textarea', 'main_prompt', false],
|
||||
nsfw_prompt: ['#nsfw_prompt_textarea', 'nsfw_prompt', false],
|
||||
jailbreak_prompt: ['#jailbreak_prompt_textarea', 'jailbreak_prompt', false],
|
||||
impersonation_prompt: ['#impersonation_prompt_textarea', 'impersonation_prompt', false],
|
||||
bias_preset_selected: ['#openai_logit_bias_preset', 'bias_preset_selected', false],
|
||||
reverse_proxy: ['#openai_reverse_proxy', 'reverse_proxy', false],
|
||||
};
|
||||
|
||||
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
|
||||
@@ -1313,16 +1315,6 @@ $(document).ready(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#oai_breakdown").on('change', function () {
|
||||
oai_settings.oai_breakdown = !!$(this).prop("checked");
|
||||
if (!oai_settings.oai_breakdown) {
|
||||
$("#token_breakdown").css('display', 'none');
|
||||
} else {
|
||||
$("#token_breakdown").css('display', 'flex');
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// auto-select a preset based on character/group name
|
||||
$(document).on("click", ".character_select", function () {
|
||||
const chid = $(this).attr('chid');
|
||||
|
@@ -4,7 +4,9 @@ import {
|
||||
chat,
|
||||
chat_metadata,
|
||||
default_avatar,
|
||||
extractMessageBias,
|
||||
getThumbnailUrl,
|
||||
replaceBiasMarkup,
|
||||
saveChatConditional,
|
||||
sendSystemMessage,
|
||||
system_avatar,
|
||||
@@ -117,6 +119,9 @@ function sendMessageAs(_, text) {
|
||||
|
||||
const name = parts.shift().trim();
|
||||
const mesText = parts.join('\n').trim();
|
||||
// Messages that do nothing but set bias will be hidden from the context
|
||||
const bias = extractMessageBias(mesText);
|
||||
const isSystem = replaceBiasMarkup(mesText).trim().length === 0;
|
||||
|
||||
const character = characters.find(x => x.name === name);
|
||||
let force_avatar, original_avatar;
|
||||
@@ -134,11 +139,14 @@ function sendMessageAs(_, text) {
|
||||
name: name,
|
||||
is_user: false,
|
||||
is_name: true,
|
||||
is_system: false,
|
||||
is_system: isSystem,
|
||||
send_date: humanizedDateTime(),
|
||||
mes: mesText,
|
||||
force_avatar: force_avatar,
|
||||
original_avatar: original_avatar,
|
||||
extra: {
|
||||
bias: bias.trim().length ? bias : null,
|
||||
}
|
||||
};
|
||||
|
||||
chat.push(message);
|
||||
@@ -152,16 +160,21 @@ function sendNarratorMessage(_, text) {
|
||||
}
|
||||
|
||||
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
|
||||
// Messages that do nothing but set bias will be hidden from the context
|
||||
const bias = extractMessageBias(text);
|
||||
const isSystem = replaceBiasMarkup(text).trim().length === 0;
|
||||
|
||||
const message = {
|
||||
name: name,
|
||||
is_user: false,
|
||||
is_name: false,
|
||||
is_system: false,
|
||||
is_system: isSystem,
|
||||
send_date: humanizedDateTime(),
|
||||
mes: text.trim(),
|
||||
force_avatar: system_avatar,
|
||||
extra: {
|
||||
type: system_message_types.NARRATOR,
|
||||
bias: bias.trim().length ? bias : null,
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -216,3 +216,19 @@ export function end_trim_to_sentence(input, include_newline = false) {
|
||||
|
||||
return input.substring(0, last + 1).trimEnd();
|
||||
}
|
||||
|
||||
export function countOccurrences(string, character) {
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
if (string[i] === character) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export function isOdd(number) {
|
||||
return number % 2 !== 0;
|
||||
}
|
@@ -427,19 +427,6 @@ code {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#token_breakdown div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.token_breakdown_segment {
|
||||
min-width: 40px !important;
|
||||
border: solid 2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
#loading_mes {
|
||||
display: none;
|
||||
width: 40px;
|
||||
@@ -921,6 +908,26 @@ select {
|
||||
}
|
||||
|
||||
|
||||
.chat_injections_list:empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat_injections_list:empty::before {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
content: "No injections";
|
||||
font-weight: bolder;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.template_parameters_list code {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0;
|
||||
@@ -3602,7 +3609,7 @@ label[for="extensions_autoconnect"] {
|
||||
|
||||
.code-copied {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
z-index: 10000;
|
||||
font-size: var(--mainFontSize);
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--SmartThemeFastUIBGColor);
|
||||
@@ -3744,6 +3751,10 @@ toolcool-color-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.flexFlowColumn {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
@@ -65,6 +65,8 @@ Get in touch with the developers directly:
|
||||
* Character emotional expressions
|
||||
* Auto-Summary of the chat history
|
||||
* Sending images to chat, and the AI interpreting the content.
|
||||
* Stable Diffusion image generation (5 chat-related presets plus 'free mode')
|
||||
* Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS)
|
||||
|
||||
## UI Extensions 🚀
|
||||
|
||||
@@ -76,6 +78,8 @@ Get in touch with the developers directly:
|
||||
| D&D Dice | A set of 7 classic D&D dice for all your dice rolling needs.<br><br>*I used to roll the dice.<br>Feel the fear in my enemies' eyes* | None | <img style="max-width:200px" alt="image" src="https://user-images.githubusercontent.com/18619528/226199925-a066c6fc-745e-4a2b-9203-1cbffa481b14.png"> |
|
||||
| Author's Note | Built-in extension that allows you to append notes that will be added to the context and steer the story and character in a specific direction. Because it's sent after the character description, it has a lot of weight. Thanks Ali឵#2222 for pitching the idea! | None |  |
|
||||
| Character Backgrounds | Built-in extension to assign unique backgrounds to specific chats or groups. | None | <img style="max-width:200px" alt="image" src="https://user-images.githubusercontent.com/18619528/233494454-bfa7c9c7-4faa-4d97-9c69-628fd96edd92.png"> |
|
||||
| Stable Diffusion | Use local of cloud-based Stable Diffusion webUI API to generate images. 5 presets included ('you', 'your face', 'me', 'the story', and 'the last message'. Free mode also supported via `/sd (anything_here_)` command in the chat input bar. Most common StableDiffusion generation settings are customizable within the SillyTavern UI. | None | <img style="max-width:200px" alt="image" src="https://files.catbox.moe/ppata8.png"> |
|
||||
| Text-to-Speech | AI-generated voice will read back character messages on demand, or automatically read new messages they arrive. Supports ElevenLabs, Silero, and your device's TTS service. | None | <img style="max-width:200px" alt="image" src="https://files.catbox.moe/o3wxkk.png"> |
|
||||
|
||||
## UI/CSS/Quality of Life tweaks by RossAscends
|
||||
|
||||
|
91
server.js
91
server.js
@@ -63,7 +63,9 @@ const utf8Encode = new TextEncoder();
|
||||
const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true });
|
||||
const commandExistsSync = require('command-exists').sync;
|
||||
|
||||
const characterCardParser = require('./src/character-card-parser.js');
|
||||
const config = require(path.join(process.cwd(), './config.conf'));
|
||||
|
||||
const server_port = process.env.SILLY_TAVERN_PORT || config.port;
|
||||
|
||||
const whitelistPath = path.join(process.cwd(), "./whitelist.txt");
|
||||
@@ -145,7 +147,6 @@ const tokenizersCache = {};
|
||||
|
||||
function getTiktokenTokenizer(model) {
|
||||
if (tokenizersCache[model]) {
|
||||
console.log('Using the cached tokenizer instance for', model);
|
||||
return tokenizersCache[model];
|
||||
}
|
||||
|
||||
@@ -913,61 +914,7 @@ async function tryReadImage(img_url, crop) {
|
||||
}
|
||||
|
||||
async function charaRead(img_url, input_format) {
|
||||
let format;
|
||||
if (input_format === undefined) {
|
||||
if (img_url.indexOf('.webp') !== -1) {
|
||||
format = 'webp';
|
||||
} else {
|
||||
format = 'png';
|
||||
}
|
||||
} else {
|
||||
format = input_format;
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case 'webp':
|
||||
try {
|
||||
const exif_data = await ExifReader.load(fs.readFileSync(img_url));
|
||||
let char_data;
|
||||
|
||||
if (exif_data['UserComment']['description']) {
|
||||
let description = exif_data['UserComment']['description'];
|
||||
if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) {
|
||||
description = exif_data['UserComment'].value[0];
|
||||
}
|
||||
try {
|
||||
json5.parse(description);
|
||||
char_data = description;
|
||||
} catch {
|
||||
const byteArr = description.split(",").map(Number);
|
||||
const uint8Array = new Uint8Array(byteArr);
|
||||
const char_data_string = utf8Decode.decode(uint8Array);
|
||||
char_data = char_data_string;
|
||||
}
|
||||
} else {
|
||||
console.log('No description found in EXIF data.');
|
||||
return false;
|
||||
}
|
||||
return char_data;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
case 'png':
|
||||
const buffer = fs.readFileSync(img_url);
|
||||
const chunks = extract(buffer);
|
||||
|
||||
const textChunks = chunks.filter(function (chunk) {
|
||||
return chunk.name === 'tEXt';
|
||||
}).map(function (chunk) {
|
||||
return PNGtext.decode(chunk.data);
|
||||
});
|
||||
var base64DecodedData = Buffer.from(textChunks[0].text, 'base64').toString('utf8');
|
||||
return base64DecodedData;//textChunks[0].text;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return characterCardParser.parse(img_url, input_format);
|
||||
}
|
||||
|
||||
app.post("/getcharacters", jsonParser, function (request, response) {
|
||||
@@ -1754,6 +1701,17 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
|
||||
return response.sendStatus(400);
|
||||
});
|
||||
|
||||
app.post("/importgroupchat", urlencodedParser, function (request, response) {
|
||||
try {
|
||||
const filedata = request.file;
|
||||
const chatname = humanizedISO8601DateTime();
|
||||
fs.copyFileSync(`./uploads/${filedata.filename}`, (`${directories.groupChats}/${chatname}.jsonl`));
|
||||
return response.send({ res: chatname });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return response.send({ error: true });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/importchat", urlencodedParser, function (request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
@@ -1763,9 +1721,8 @@ app.post("/importchat", urlencodedParser, function (request, response) {
|
||||
let avatar_url = (request.body.avatar_url).replace('.png', '');
|
||||
let ch_name = request.body.character_name;
|
||||
if (filedata) {
|
||||
|
||||
if (format === 'json') {
|
||||
fs.readFile('./uploads/' + filedata.filename, 'utf8', (err, data) => {
|
||||
fs.readFile(`./uploads/${filedata.filename}`, 'utf8', (err, data) => {
|
||||
|
||||
if (err) {
|
||||
console.log(err);
|
||||
@@ -1782,7 +1739,6 @@ app.post("/importchat", urlencodedParser, function (request, response) {
|
||||
user_name: 'You',
|
||||
character_name: ch_name,
|
||||
create_date: humanizedISO8601DateTime(),
|
||||
|
||||
},
|
||||
...history.msgs.map(
|
||||
(message) => ({
|
||||
@@ -1803,7 +1759,7 @@ app.post("/importchat", urlencodedParser, function (request, response) {
|
||||
|
||||
const errors = [];
|
||||
newChats.forEach(chat => fs.writeFile(
|
||||
chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + ' imported.jsonl',
|
||||
`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`,
|
||||
chat.map(JSON.stringify).join('\n'),
|
||||
'utf8',
|
||||
(err) => err ?? errors.push(err)
|
||||
@@ -1832,8 +1788,7 @@ app.post("/importchat", urlencodedParser, function (request, response) {
|
||||
let jsonData = json5.parse(line);
|
||||
|
||||
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
|
||||
//console.log(humanizedISO8601DateTime()+':/importchat copying chat as '+ch_name+' - '+humanizedISO8601DateTime()+'.jsonl');
|
||||
fs.copyFile('./uploads/' + filedata.filename, chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + '.jsonl', (err) => { //added character name and replaced Date.now() with humanizedISO8601DateTime
|
||||
fs.copyFile(`./uploads/${filedata.filename}`, (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`), (err) => {
|
||||
if (err) {
|
||||
response.send({ error: true });
|
||||
return console.log(err);
|
||||
@@ -1849,9 +1804,7 @@ app.post("/importchat", urlencodedParser, function (request, response) {
|
||||
rl.close();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.post('/importworldinfo', urlencodedParser, (request, response) => {
|
||||
@@ -1919,7 +1872,7 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
|
||||
const crop = tryParse(request.query.crop);
|
||||
let rawImg = await jimp.read(pathToUpload);
|
||||
|
||||
if (typeof crop == 'object') {
|
||||
if (typeof crop == 'object' && [crop.x, crop.y, crop.width, crop.height].every(x => typeof x === 'number')) {
|
||||
rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height);
|
||||
}
|
||||
|
||||
@@ -2746,8 +2699,12 @@ const setupTasks = async function () {
|
||||
}
|
||||
|
||||
if (listen && !config.whitelistMode && !config.basicAuthMode) {
|
||||
console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.');
|
||||
process.exit(1);
|
||||
if (config.securityOverride)
|
||||
console.warn("Security has been override. If it's not a trusted network, change the settings.");
|
||||
else {
|
||||
console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (true === cliArguments.ssl)
|
||||
|
72
src/character-card-parser.js
Normal file
72
src/character-card-parser.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const fs = require('fs');
|
||||
const json5 = require('json5');
|
||||
const ExifReader = require('exifreader');
|
||||
|
||||
const extract = require('png-chunks-extract');
|
||||
const PNGtext = require('png-chunk-text');
|
||||
|
||||
const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true });
|
||||
|
||||
const parse = async (cardUrl, format) => {
|
||||
let fileFormat;
|
||||
if (format === undefined) {
|
||||
if (cardUrl.indexOf('.webp') !== -1)
|
||||
fileFormat = 'webp';
|
||||
else
|
||||
fileFormat = 'png';
|
||||
}
|
||||
else
|
||||
fileFormat = format;
|
||||
|
||||
switch (fileFormat) {
|
||||
case 'webp':
|
||||
try {
|
||||
const exif_data = await ExifReader.load(fs.readFileSync(cardUrl));
|
||||
let char_data;
|
||||
|
||||
if (exif_data['UserComment']['description']) {
|
||||
let description = exif_data['UserComment']['description'];
|
||||
if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) {
|
||||
description = exif_data['UserComment'].value[0];
|
||||
}
|
||||
|
||||
try {
|
||||
json5.parse(description);
|
||||
char_data = description;
|
||||
} catch {
|
||||
const byteArr = description.split(",").map(Number);
|
||||
const uint8Array = new Uint8Array(byteArr);
|
||||
const char_data_string = utf8Decode.decode(uint8Array);
|
||||
char_data = char_data_string;
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('No description found in EXIF data.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return char_data;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
case 'png':
|
||||
const buffer = fs.readFileSync(cardUrl);
|
||||
const chunks = extract(buffer);
|
||||
|
||||
const textChunks = chunks.filter(function (chunk) {
|
||||
return chunk.name === 'tEXt';
|
||||
}).map(function (chunk) {
|
||||
return PNGtext.decode(chunk.data);
|
||||
});
|
||||
|
||||
return Buffer.from(textChunks[0].text, 'base64').toString('utf8');
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
parse: parse
|
||||
};
|
Reference in New Issue
Block a user