This commit is contained in:
RossAscends
2023-04-23 05:07:29 +09:00
38 changed files with 906 additions and 515 deletions

32
.github/workflows/npm-publish.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ config.conf
public/settings.json
/thumbnails
whitelist.txt
.vscode

4
.npmignore Normal file
View File

@ -0,0 +1,4 @@
node_modules/
/uploads/
.DS_Store
/thumbnails

174
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "TavernAI",
"version": "1.3.0",
"name": "sillytavern",
"version": "1.4.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "TavernAI",
"version": "1.3.0",
"name": "sillytavern",
"version": "1.4.8",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
"axios": "^1.3.4",
@ -31,10 +31,11 @@
"rimraf": "^3.0.2",
"sanitize-filename": "^1.6.3",
"webp-converter": "2.3.2",
"ws": "^8.13.0"
"ws": "^8.13.0",
"yargs": "^17.7.1"
},
"bin": {
"TavernAI": "server.js"
"sillytavern": "server.js"
}
},
"node_modules/@dqbd/tiktoken": {
@ -476,6 +477,28 @@
"node": ">= 0.6"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/any-base": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
@ -652,6 +675,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -880,6 +932,11 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -888,6 +945,14 @@
"node": ">= 0.8"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -1073,6 +1138,14 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
@ -1245,6 +1318,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
@ -1779,6 +1860,14 @@
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -1937,6 +2026,30 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strtok3": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
@ -2083,6 +2196,22 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -2151,6 +2280,39 @@
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
}
}
}

View File

@ -23,17 +23,21 @@
"rimraf": "^3.0.2",
"sanitize-filename": "^1.6.3",
"webp-converter": "2.3.2",
"ws": "^8.13.0"
"ws": "^8.13.0",
"yargs": "^17.7.1"
},
"overrides": {
"parse-bmfont-xml": {
"xml2js": "^0.5.0"
}
},
"name": "TavernAI",
"version": "1.3.0",
"name": "sillytavern",
"version": "1.4.8",
"scripts": {
"start": "node server.js"
},
"bin": {
"TavernAI": "server.js"
"sillytavern": "./server.js"
},
"rules": {
"no-path-concat": "off",

View File

@ -1416,13 +1416,27 @@
<div title="Token counts may be inaccurate and provided just for reference." id="result_info"></div>
</div>
<hr>
<div id="description_div" class="margin-bot-10px">
Description
<a href="/notes/1" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
<div id="fav_chara_wrap">
<div id="fav_chara_label" class="margin-bot-10px">
<label for="fav_checkbox" class="checkbox_label">
<input type="checkbox" id="fav_checkbox" name="fav"/>
Favorite
<a href="/notes/15" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
</label>
</div>
<div>
</div>
</div>
</div>
<div id="description_div" class="margin-bot-10px">
Description
<a href="/notes/1" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
</div>
<textarea id="description_textarea" placeholder="Describe your character's physical and mental traits here." class="margin-bot-10px" name="description" placeholder=""></textarea>
<div id="first_message_div" class="margin-bot-10px">
@ -1459,6 +1473,10 @@
</div>
<div id="rm_group_buttons">
<div class="rm_group_settings">
<label class="checkbox_label">
<input id="rm_group_fav" type="checkbox" />
Favorite
</label>
<label class="checkbox_label">
<input id="rm_group_allow_self_responses" type="checkbox" />
Allow bot responses to self
@ -1518,6 +1536,8 @@
<div class="fa-solid fa-user-group"></div>
</div>
<div class="ch_name"></div>
<i class='group_fav_icon fa-solid fa-star fa-2xs'></i>
<input class="ch_fav" value="" hidden />
</div>
</div>
</div>
@ -1533,6 +1553,7 @@
<div id="rm_button_create" title="Create New Character" class="menu_button fa-solid fa-user-plus "></div>
<div id="character_import_button" title="Import Character from File" class="menu_button fa-solid fa-file-arrow-up "></div>
<div id="rm_button_group_chats" title="Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
<div id="filter_by_fav" title="Filter By Favorite" class="menu_button fa-solid fa-star"></div>
</div>
<form id="form_character_search_form" action="javascript:void(null);">
<input id="character_search_bar" class="text_pole" type="search" placeholder="Character search..." maxlength="50" />

View File

@ -1,47 +1,47 @@
<html>
<head>
<title>SillyTavern - Note - Character Descriptions</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<title>Character Descriptions</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>Character description</h2>
<p>
Used to add the character description and the rest that the AI should know.<br><br>
For example, you can add information about the world in which the action takes place and describe
the characteristics for the character you are playing for.<br><br>
Usually it all takes 200-350 tokens.
</p>
<h3>Methods and format</h3>
<p>
For most Kobold's models the easiest way is to use a free form for description, and in each sentence it is desirable to specify the name of the character.<br><br>
The entire description should be in one line without hyphenation.<br><br>
For example:<br><br>
<code>
Chloe is a female elf. Chloe wears black-white maid dress with green collar and red glasses. Chloe has medium length black hair. Chloe's personality is...
</code>
</p>
<div id="main">
<div id="content">
<h2>Character description</h2>
<p>
Used to add the character description and the rest that the AI should know.<br><br>
For example, you can add information about the world in which the action takes place and describe
the characteristics for the character you are playing for.<br><br>
Usually it all takes 200-350 tokens.
</p>
<h3>Methods and format</h3>
<p>
For most Kobold's models the easiest way is to use a free form for description, and in each sentence it
is desirable to specify the name of the character.<br><br>
The entire description should be in one line without hyphenation.<br><br>
For example:<br><br>
<code>
Chloe is a female elf. Chloe wears black-white maid dress with green collar and red glasses. Chloe has medium length black hair. Chloe's personality is...
</code>
</p>
<p>
But that the AI would be less confused the best way is to use the W++ format.<Br><br>
Details here: <a target="_blank" href="https://github.com/KoboldAI/KoboldAI-Client/wiki/Pro-Tips">Pro-Tips</a>
</p>
<hr>
<p>
<u>A list of tags that are replaced when sending to generate:</u><br><br>
{{user}} and &lt;USER&gt; are replaced by the User's Name<br>
{{char}} and &lt;BOT&gt; are replaced by the Character's Name
</p>
</div>
</div>
<p>
But that the AI would be less confused the best way is to use the W++ format.<Br><br>
Details here: <a target="_blank"
href="https://github.com/KoboldAI/KoboldAI-Client/wiki/Pro-Tips">Pro-Tips</a>
</p>
<hr>
<p>
<u>A list of tags that are replaced when sending to generate:</u><br><br>
{{user}} and &lt;USER&gt; are replaced by the User's Name<br>
{{char}} and &lt;BOT&gt; are replaced by the Character's Name
</p>
</div>
</div>
</body>
</html>

View File

@ -1,13 +1,11 @@
<html>
<head>
<title>SillyTavern - Note - Import Chat</title>
<title>Import Chat</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -1,13 +1,11 @@
<html>
<head>
<title>SillyTavern - Note - Example Dialogues</title>
<title>Example Dialogues</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -1,13 +1,11 @@
<html>
<head>
<title>SillyTavern - Note - Scenario</title>
<title>Scenario</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
@ -15,15 +13,20 @@
<div id="content">
<h2>World Info</h2>
<h4>World Info enhances AI's understanding of the details in your world.</h4>
<p>It functions like a dynamic dictionary that only inserts relevant information from World Info entries when keywords associated with the entries are present in the message text.</p>
<p>The SillyTavern engine activates and seamlessly integrates the appropriate lore into the prompt, providing background information to the AI.</p>
<p><i>It is important to note that while World Info helps guide the AI towards your desired lore, it does not guarantee its appearance in the generated output messages.</i></p>
<p>It functions like a dynamic dictionary that only inserts relevant information from World Info entries
when keywords associated with the entries are present in the message text.</p>
<p>The SillyTavern engine activates and seamlessly integrates the appropriate lore into the prompt,
providing background information to the AI.</p>
<p><i>It is important to note that while World Info helps guide the AI towards your desired lore, it does
not guarantee its appearance in the generated output messages.</i></p>
<h3>Pro Tips</h3>
<ul>
<li>The AI does not insert keywords into context, so each World Info entry should be a comprehensive, standalone description.</li>
<li>The AI does not insert keywords into context, so each World Info entry should be a comprehensive,
standalone description.</li>
<li>To create a rich and detailed world lore, entries can be interlinked and reference one another.</li>
<li>To conserve tokens, it is advisable to keep entry contents concise, with a general recommended limit of 50 tokens per entry.</li>
<li>To conserve tokens, it is advisable to keep entry contents concise, with a general recommended limit
of 50 tokens per entry.</li>
</ul>
</div>
</div>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -4,10 +4,8 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
</head>
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">

22
public/notes/15.html Normal file
View File

@ -0,0 +1,22 @@
<html>
<head>
<title>Favorite Character</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>Favorite Character</h2>
<p>
Mark character as favorite to quickly filter on the side menu bar by pressing the star button.
</p>
</div>
</div>
</body>
</html>

View File

@ -1,13 +1,11 @@
<html>
<head>
<title>SillyTavern - Note - Personality Summary</title>
<title>Personality Summary</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
@ -15,7 +13,8 @@
<div id="content">
<h2>Personality summary</h2>
<p>
A brief description of the personality. It is added to the chat at a depth of 8-15 messages, so it has a significant impact on the character.
A brief description of the personality. It is added to the chat at a depth of 8-15 messages, so it has a
significant impact on the character.
</p>
Example:

View File

@ -1,40 +1,40 @@
<html>
<head>
<title>SillyTavern - Note - First Message</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<title>First Message</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>First message</h2>
<p>
The First Message is an important thing that sets exactly how and in what style the character will communicate.<Br><br>
It is desirable that the character's first message be long, so that later it would be less likely that the character would respond in with very short messages. <br><br>
You can also use asterisks ** to describe the character's actions.
</P>
<div id="main">
<div id="content">
<h2>First message</h2>
<p>
The First Message is an important thing that sets exactly how and in what style the character will
communicate.<Br><br>
It is desirable that the character's first message be long, so that later it would be less likely that
the character would respond in with very short messages. <br><br>
You can also use asterisks ** to describe the character's actions.
</P>
For example:
<br><br>
<code>
For example:
<br><br>
<code>
*I noticed you came inside, I walked up and stood right in front of you* Welcome. I'm glad to see you here.
*I said with toothy smug sunny smile looking you straight in the eye* What brings you...
</code>
<Br>
<hr>
<p>
<u>A list of tags that are replaced when sending to generate:</u><br><br>
{{user}} and &lt;USER&gt; are replaced by the User's Name<br>
{{char}} and &lt;BOT&gt; are replaced by the Character's Name
</p>
</div>
</div>
<Br>
<hr>
<p>
<u>A list of tags that are replaced when sending to generate:</u><br><br>
{{user}} and &lt;USER&gt; are replaced by the User's Name<br>
{{char}} and &lt;BOT&gt; are replaced by the Character's Name
</p>
</div>
</div>
</body>
</html>

View File

@ -1,13 +1,11 @@
<html>
<head>
<title>SillyTavern - Note - KobolAI Settings</title>
<title>KoboldAI Settings</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -1,36 +1,37 @@
<html>
<head>
<title>SillyTavern - Note - Novel AI API Key</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<title>NovelAI API Key</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>Finding your NAI API key</h2>
<h4>To get a NovelAI API key, follow these instructions:</h4>
<p>
1. Go to the NovelAI website and Login.<br><br>
<div id="main">
<div id="content">
<h2>Finding your NAI API key</h2>
<h4>To get a NovelAI API key, follow these instructions:</h4>
<p>
1. Go to the NovelAI website and Login.<br><br>
2. Create a new story, or open an existing story.<br><br>
2. Create a new story, or open an existing story.<br><br>
3. Open the Network Tools on your web browser. (For Chrome or Firefox, you do this by pressing Ctrl+Shift+I, then switching to the Network tab.)<br><br>
3. Open the Network Tools on your web browser. (For Chrome or Firefox, you do this by pressing
Ctrl+Shift+I, then switching to the Network tab.)<br><br>
4. Generate something. You should see two requests to api.novelai.net/ai/generate-stream, which might look something like this:<br><br>
<img src="1.png"><br><br>
5. Select the second request, then in the Headers tab of the inspection panel, scroll down to the very bottom. Look for a header called Authorization:<br><br>
<img src="2.png"><br><br>
The long string (after "Bearer", not including it) is your API key.<br><br>
* Proxies and Cloudflare-type services may interfere with connection.
</p>
</div>
</div>
4. Generate something. You should see two requests to api.novelai.net/ai/generate-stream, which might
look something like this:<br><br>
<img src="1.png"><br><br>
5. Select the second request, then in the Headers tab of the inspection panel, scroll down to the very
bottom. Look for a header called Authorization:<br><br>
<img src="2.png"><br><br>
The long string (after "Bearer", not including it) is your API key.<br><br>
* Proxies and Cloudflare-type services may interfere with connection.
</p>
</div>
</div>
</body>
</html>

View File

@ -1,43 +1,43 @@
<html>
<head>
<title>SillyTavern - Note - NovelAI Settings</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<title>NovelAI Settings</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>NovelAI settings</h2>
<p>
The files with the settings are here (SillyTavern\public\NovelAI Settings).<br>
You can also manually add your own settings files.
</p>
<h3>Temperature</h3>
<p>
Value from 0.1 to 2.0. <br>
Lower value - the answers are more logical, but less creative. <br>
Higher value - the answers are more creative, but less logical.
</p>
<div id="main">
<div id="content">
<h2>NovelAI settings</h2>
<p>
The files with the settings are here (SillyTavern\public\NovelAI Settings).<br>
You can also manually add your own settings files.
</p>
<h3>Temperature</h3>
<p>
Value from 0.1 to 2.0. <br>
Lower value - the answers are more logical, but less creative. <br>
Higher value - the answers are more creative, but less logical.
</p>
<h3>Repetition penalty</h3>
<p>
Repetition penalty is responsible for the penalty of repeated words. <br>
If the character is fixated on something or repeats the same phrase, then increasing this parameter will fix it. <br>
It is not recommended to increase this parameter too much for the chat format, as it may break this format. <br>
<b>The standard value for chat is approximately 1.0 - 1.05</b>
</p>
<h3>Repetition penalty range</h3>
<p>
The range of influence of Repetition penalty in tokens.
</p>
</div>
</div>
<h3>Repetition penalty</h3>
<p>
Repetition penalty is responsible for the penalty of repeated words. <br>
If the character is fixated on something or repeats the same phrase, then increasing this parameter will
fix it. <br>
It is not recommended to increase this parameter too much for the chat format, as it may break this
format. <br>
<b>The standard value for chat is approximately 1.0 - 1.05</b>
</p>
<h3>Repetition penalty range</h3>
<p>
The range of influence of Repetition penalty in tokens.
</p>
</div>
</div>
</body>
</html>

View File

@ -1,13 +1,11 @@
<html>
<head>
<title>SillyTavern - Note - NovelAI Models</title>
<title>NovelAI Models</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -1,49 +1,53 @@
<html>
<head>
<title>SillyTavern - Note - Anchors</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<title>Anchors</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>Anchors</h2>
<p>
Anchors are used to increase the length of messages.<br>
There are two types of anchors: <u>Character Anchor</u> and <u>Style Anchor</u>
</p>
<p>
<u>Character Anchor</u> - affects the character played by the AI by motivating it to write longer messages.<br><br>
Looks like:
<code>[Elaborate speaker]</code>
</p>
<p>
<u>Style Anchor</u> - affects the entire AI model, motivating the AI to write longer messages even when it is not acting as the character.<Br><br>
Looks like:
<code>[Writing style: very long messages]</code>
</p>
<hr>
<p>
Anchors Order sets the location of anchors in the promt, the first anchor in the order is much further back in the context and thus has less influence than second.
</p>
<p>
The second anchor is only turned on after 8-12 messages, because when the chat still only has a few messages, the first anchor creates enough effect on its own.
</p>
<p>
Sometimes an AI model may not perceive anchors correctly or the AI model already generates sufficiently long messages.
For these cases, you can disable the anchors by unchecking their respective boxes.
</p>
<p>
<u>When using Pygmalion models these anchors are automatically disabled, since Pygmalion already generates long enough messages.</u>
</p>
</div>
</div>
<div id="main">
<div id="content">
<h2>Anchors</h2>
<p>
Anchors are used to increase the length of messages.<br>
There are two types of anchors: <u>Character Anchor</u> and <u>Style Anchor</u>
</p>
<p>
<u>Character Anchor</u> - affects the character played by the AI by motivating it to write longer
messages.<br><br>
Looks like:
<code>[Elaborate speaker]</code>
</p>
<p>
<u>Style Anchor</u> - affects the entire AI model, motivating the AI to write longer messages even when
it is not acting as the character.<Br><br>
Looks like:
<code>[Writing style: very long messages]</code>
</p>
<hr>
<p>
Anchors Order sets the location of anchors in the promt, the first anchor in the order is much further
back in the context and thus has less influence than second.
</p>
<p>
The second anchor is only turned on after 8-12 messages, because when the chat still only has a few
messages, the first anchor creates enough effect on its own.
</p>
<p>
Sometimes an AI model may not perceive anchors correctly or the AI model already generates sufficiently
long messages.
For these cases, you can disable the anchors by unchecking their respective boxes.
</p>
<p>
<u>When using Pygmalion models these anchors are automatically disabled, since Pygmalion already
generates long enough messages.</u>
</p>
</div>
</div>
</body>
</html>

View File

@ -5,11 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap"
rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,11 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap"
rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,11 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap"
rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,11 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap"
rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -5,9 +5,7 @@
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&amp;display=swap" rel="stylesheet">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>

View File

@ -159,7 +159,7 @@ export {
}
// API OBJECT FOR EXTERNAL WIRING
window["TavernAI"] = {};
window["SillyTavern"] = {};
let converter = new showdown.Converter({ emoji: "true" });
const gpt3 = new GPT3BrowserTokenizer({ type: 'gpt3' });
@ -204,6 +204,8 @@ let dialogueResolve = null;
let chat_metadata = {};
let streamingProcessor = null;
let fav_ch_checked = false;
window.filterByFav = false;
const durationSaveEdit = 200;
const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
@ -330,6 +332,7 @@ var menu_type = ""; //what is selected in the menu
var selected_button = ""; //which button pressed
//create pole save
var create_save_name = "";
var create_fav_chara = "";
var create_save_description = "";
var create_save_personality = "";
var create_save_first_message = "";
@ -635,8 +638,6 @@ function updateSoftPromptsList(soft_prompts) {
}
function printCharacters() {
//console.log('printCharacters() entered');
$("#rm_print_characters_block").empty();
//console.log('printCharacters() -- sees '+characters.length+' characters.');
characters.forEach(function (item, i, arr) {
@ -648,7 +649,8 @@ function printCharacters() {
`<div class=character_select chid=${i} id="CharID${i}">
<div class=avatar><img src="${this_avatar}"></div>
<div class=ch_name>${item.name}</div>
<div class=ch_name>${item.name} ${item.fav == "true" ? '<i class="fa-solid fa-star fa-2xs"></i>' : ''}</div>
<input class="ch_fav" value=${item.fav} hidden />
</div>`
);
//console.log('printcharacters() -- printing -- ChID '+i+' ('+item.name+')');
@ -1314,7 +1316,7 @@ class StreamingProcessor {
}
async function Generate(type, automatic_trigger, force_name2) {
console.log('Generate entered');
//console.log('Generate entered');
setGenerationProgress(0);
tokens_already_generated = 0;
const isImpersonate = type == "impersonate";
@ -3166,6 +3168,9 @@ function select_selected_character(chid) {
if (characters[chid].avatar != "none") {
this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
}
$("#fav_checkbox").prop("checked", characters[chid].fav == "true");
$("#avatar_load_preview").attr("src", this_avatar);
$("#name_div").css("display", "none");
@ -3456,7 +3461,7 @@ function isHordeGenerationNotAllowed() {
return false;
}
window["TavernAI"].getContext = function () {
window["SillyTavern"].getContext = function () {
return {
chat: chat,
characters: characters,
@ -3791,6 +3796,25 @@ $(document).ready(function () {
}
});
$("#filter_by_fav").click(function() {
filterByFav = !filterByFav;
const selector = ['#rm_print_characters_block .character_select', '#rm_print_characters_block .group_select'].join(',');
if(filterByFav){
$(selector).each(function () {
if($(this).children(".ch_fav").length !== 0){
$(this).children(".ch_fav").val().toLowerCase().includes(true)
? $(this).show()
: $(this).hide();
}
});
$("#filter_by_fav").addClass("fav_on");
}else{
$(selector).show();
$("#filter_by_fav").removeClass("fav_on");
}
});
$("#send_but").click(function () {
if (is_send_press == false) {
is_send_press = true;
@ -3833,6 +3857,7 @@ $(document).ready(function () {
selected_button = "character_edit";
select_selected_character(this_chid);
}
$("#character_search_bar").val("").trigger("input");
});
$(document).on("click", ".character_select", function () {
@ -3860,7 +3885,6 @@ $(document).ready(function () {
selected_button = "character_edit";
select_selected_character(this_chid);
}
$("#character_search_bar").val("").trigger("input");
});
@ -4128,6 +4152,7 @@ $(document).ready(function () {
$("#rm_info_avatar").html("");
let save_name = create_save_name;
var formData = new FormData($("#form_create").get(0));
formData.set('fav', fav_ch_checked);
if ($("#form_create").attr("actiontype") == "createcharacter") {
if ($("#character_name_pole").val().length > 0) {
//if the character name text area isn't empty (only posible when creating a new character)
@ -4294,11 +4319,18 @@ $(document).ready(function () {
create_save_scenario = $("#scenario_pole").val();
create_save_mes_example = $("#mes_example_textarea").val();
create_save_first_message = $("#firstmessage_textarea").val();
create_fav_chara = $("#fav_checkbox").val();
} else {
saveCharacterDebounced();
}
});
$("#fav_checkbox").change(function(){
fav_ch_checked = $(this).prop("checked");
if (menu_type != "create") {
saveCharacterDebounced();
}
});
$("#talkativeness_slider").on("input", function () {
if (menu_type == "create") {

View File

@ -30,7 +30,7 @@ const extension_settings = {
let modules = [];
let activeExtensions = new Set();
const getContext = () => window['TavernAI'].getContext();
const getContext = () => window['SillyTavern'].getContext();
const getApiUrl = () => extension_settings.apiUrl;
const defaultRequestArgs = { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } };
let connectedToApi = false;

View File

@ -0,0 +1,102 @@
export { ElevenLabsTtsProvider }
class ElevenLabsTtsProvider {
API_KEY
set API_KEY(apiKey) {
this.API_KEY = apiKey
}
get API_KEY() {
return this.API_KEY
}
async fetchTtsVoiceIds() {
const headers = {
'xi-api-key': this.API_KEY
}
const response = await fetch(`https://api.elevenlabs.io/v1/voices`, {
headers: headers
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
const responseJson = await response.json()
return responseJson.voices
}
async fetchTtsVoiceSettings() {
const headers = {
'xi-api-key': this.API_KEY
}
const response = await fetch(
`https://api.elevenlabs.io/v1/voices/settings/default`,
{
headers: headers
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
return response.json()
}
async fetchTtsGeneration(text, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: 'POST',
headers: {
'xi-api-key': this.API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: text })
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
return response
}
async fetchTtsFromHistory(history_item_id) {
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`)
const response = await fetch(
`https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,
{
headers: {
'xi-api-key': this.API_KEY
}
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
return response
}
async fetchTtsHistory() {
const headers = {
'xi-api-key': this.API_KEY
}
const response = await fetch(`https://api.elevenlabs.io/v1/history`, {
headers: headers
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
const responseJson = await response.json()
return responseJson.history
}
async findTtsGenerationInHistory(message, voiceId) {
const ttsHistory = await this.fetchTtsHistory()
for (const history of ttsHistory) {
const text = history.text
const itemId = history.history_item_id
if (message === text && history.voice_id == voiceId) {
console.info(`Existing TTS history item ${itemId} found: ${text} `)
return itemId
}
}
return ''
}
}

View File

@ -1,160 +1,80 @@
import { callPopup, saveSettingsDebounced } from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js";
import { getStringHash } from "../../utils.js";
import { callPopup, saveSettingsDebounced } from '../../../script.js'
import { extension_settings, getContext } from '../../extensions.js'
import { getStringHash } from '../../utils.js'
import { ElevenLabsTtsProvider } from './elevenlabs.js'
const UPDATE_INTERVAL = 1000;
let API_KEY
const UPDATE_INTERVAL = 1000
let voiceMap = {} // {charName:voiceid, charName2:voiceid2}
let elevenlabsTtsVoices = []
let audioControl
let lastCharacterId = null
let lastGroupId = null
let lastChatId = null
let lastMessageHash = null
let lastCharacterId = null;
let lastGroupId = null;
let lastChatId = null;
let lastMessageHash = null;
let ttsProvider = new ElevenLabsTtsProvider()
async function moduleWorker() {
// Primarily determinign when to add new chat to the TTS queue
const enabled = $("#elevenlabs_enabled").is(':checked');
const enabled = $('#elevenlabs_enabled').is(':checked')
if (!enabled) {
return;
return
}
const context = getContext()
const chat = context.chat;
const chat = context.chat
processTtsQueue();
processAudioJobQueue();
updateUiAudioPlayState();
processTtsQueue()
processAudioJobQueue()
updateUiAudioPlayState()
// no characters or group selected
if (!context.groupId && !context.characterId) {
return;
return
}
// Chat/character/group changed
if ((context.groupId && lastGroupId !== context.groupId) || (context.characterId !== lastCharacterId) || (context.chatId !== lastChatId)) {
if (
(context.groupId && lastGroupId !== context.groupId) ||
context.characterId !== lastCharacterId ||
context.chatId !== lastChatId
) {
currentMessageNumber = context.chat.length ? context.chat.length : 0
saveLastValues();
return;
saveLastValues()
return
}
// take the count of messages
let lastMessageNumber = context.chat.length ? context.chat.length : 0;
let lastMessageNumber = context.chat.length ? context.chat.length : 0
// There's no new messages
let diff = lastMessageNumber - currentMessageNumber;
let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '');
let diff = lastMessageNumber - currentMessageNumber
let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '')
if (diff == 0 && hashNew === lastMessageHash) {
return;
return
}
const message = chat[chat.length - 1];
const message = chat[chat.length - 1]
// We're currently swiping or streaming. Don't generate voice
if (message.mes === '...' || (context.streamingProcessor && !context.streamingProcessor.isFinished)) {
return;
if (
message.mes === '...' ||
(context.streamingProcessor && !context.streamingProcessor.isFinished)
) {
return
}
// New messages, add new chat to history
lastMessageHash = hashNew;
currentMessageNumber = lastMessageNumber;
lastMessageHash = hashNew
currentMessageNumber = lastMessageNumber
console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
ttsJobQueue.push(message);
}
//#################//
// TTS API Calls //
//#################//
async function fetchTtsVoiceIds() {
const headers = {
'xi-api-key': API_KEY
};
const response = await fetch(`https://api.elevenlabs.io/v1/voices`, {
headers: headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const responseJson = await response.json();
return responseJson.voices;
}
async function fetchTtsVoiceSettings() {
const headers = {
'xi-api-key': API_KEY
};
const response = await fetch(`https://api.elevenlabs.io/v1/voices/settings/default`, {
headers: headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
return response.json();
}
async function fetchTtsGeneration(text, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
method: 'POST',
headers: {
'xi-api-key': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: text })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
return response;
}
async function fetchTtsFromHistory(history_item_id) {
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`);
const response = await fetch(`https://api.elevenlabs.io/v1/history/${history_item_id}/audio`, {
headers: {
'xi-api-key': API_KEY
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
return response;
}
async function fetchTtsHistory() {
const headers = {
'xi-api-key': API_KEY
};
const response = await fetch(`https://api.elevenlabs.io/v1/history`, {
headers: headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const responseJson = await response.json();
return responseJson.history;
}
async function findTtsGenerationInHistory(message, voiceId) {
const ttsHistory = await fetchTtsHistory();
for (const history of ttsHistory) {
const text = history.text;
const itemId = history.history_item_id;
if (message === text && history.voice_id == voiceId) {
console.info(`Existing TTS history item ${itemId} found: ${text} `)
return itemId;
}
}
return ''
console.debug(
`Adding message from ${message.name} for TTS processing: "${message.mes}"`
)
ttsJobQueue.push(message)
}
//##################//
@ -170,14 +90,13 @@ let queueProcessorReady = true
let lastAudioPosition = 0
async function playAudioData(audioBlob) {
const reader = new FileReader();
const reader = new FileReader()
reader.onload = function (e) {
const srcUrl = e.target.result;
audioElement.src = srcUrl;
};
reader.readAsDataURL(audioBlob);
const srcUrl = e.target.result
audioElement.src = srcUrl
}
reader.readAsDataURL(audioBlob)
audioElement.addEventListener('ended', completeCurrentAudioJob)
audioElement.addEventListener('canplay', () => {
console.debug(`Starting TTS playback`)
@ -185,28 +104,26 @@ async function playAudioData(audioBlob) {
})
}
window['elevenlabsPreview'] = function(id) {
const audio = document.getElementById(id);
audio.play();
window['elevenlabsPreview'] = function (id) {
const audio = document.getElementById(id)
audio.play()
}
async function onElevenlabsVoicesClick() {
let popupText = '';
let popupText = ''
try {
const voiceIds = await fetchTtsVoiceIds();
const voiceIds = await ttsProvider.fetchTtsVoiceIds()
for (const voice of voiceIds) {
popupText += `<div class="voice_preview"><b>${voice.name}</b> <i onclick="elevenlabsPreview('${voice.voice_id}')" class="fa-solid fa-play"></i></div>`;
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}"></audio>`;
popupText += `<div class="voice_preview"><b>${voice.name}</b> <i onclick="elevenlabsPreview('${voice.voice_id}')" class="fa-solid fa-play"></i></div>`
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}"></audio>`
}
}
catch {
} catch {
popupText = 'Could not load voices list. Check your API key.'
}
callPopup(popupText, 'text');
callPopup(popupText, 'text')
}
function completeCurrentAudioJob() {
@ -221,17 +138,17 @@ function completeCurrentAudioJob() {
*/
async function addAudioJob(response) {
const audioData = await response.blob()
if (audioData.type != "audio/mpeg") {
if (audioData.type != 'audio/mpeg') {
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
}
audioJobQueue.push(audioData)
console.debug("Pushed audio job to queue.")
console.debug('Pushed audio job to queue.')
}
async function processAudioJobQueue() {
// Nothing to do, audio not completed, or audio paused - stop processing.
if (audioJobQueue.length == 0 || !queueProcessorReady || audioPaused) {
return;
return
}
try {
queueProcessorReady = false
@ -243,7 +160,6 @@ async function processAudioJobQueue() {
}
}
//################//
// TTS Control //
//################//
@ -259,22 +175,24 @@ function completeTtsJob() {
function saveLastValues() {
const context = getContext()
lastGroupId = context.groupId;
lastCharacterId = context.characterId;
lastChatId = context.chatId;
lastMessageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
lastGroupId = context.groupId
lastCharacterId = context.characterId
lastChatId = context.chatId
lastMessageHash = getStringHash(
(context.chat.length && context.chat[context.chat.length - 1].mes) ?? ''
)
}
async function tts(text, voiceId) {
const historyId = await findTtsGenerationInHistory(text, voiceId);
const historyId = await ttsProvider.findTtsGenerationInHistory(text, voiceId)
let response;
let response
if (historyId) {
console.debug(`Found existing TTS generation with id ${historyId}`)
response = await fetchTtsFromHistory(historyId);
response = await ttsProvider.fetchTtsFromHistory(historyId)
} else {
console.debug(`No existing TTS generation found, requesting new generation`)
response = await fetchTtsGeneration(text, voiceId);
response = await ttsProvider.fetchTtsGeneration(text, voiceId)
}
addAudioJob(response)
completeTtsJob()
@ -283,12 +201,12 @@ async function tts(text, voiceId) {
async function processTtsQueue() {
// Called each moduleWorker iteration to pull chat messages from queue
if (currentTtsJob || ttsJobQueue.length <= 0 || audioPaused) {
return;
return
}
console.debug("New message found, running TTS")
console.debug('New message found, running TTS')
currentTtsJob = ttsJobQueue.shift()
const text = currentTtsJob.mes.replaceAll('*', '...');
const text = currentTtsJob.mes.replaceAll('*', '...')
const char = currentTtsJob.name
try {
@ -298,20 +216,19 @@ async function processTtsQueue() {
const voice = await getTtsVoice(voiceMap[char])
const voiceId = voice.voice_id
if (voiceId == null) {
throw (`Unable to attain voiceId for ${char}`)
throw `Unable to attain voiceId for ${char}`
}
tts(text, voiceId)
} catch (error) {
console.error(error)
currentTtsJob = null
}
}
// Secret function for now
async function playFullConversation() {
const context = getContext()
const chat = context.chat;
const chat = context.chat
ttsJobQueue = chat
}
window.playFullConversation = playFullConversation
@ -323,52 +240,60 @@ window.playFullConversation = playFullConversation
function loadSettings() {
const context = getContext()
if (Object.keys(extension_settings.elevenlabstts).length === 0) {
Object.assign(extension_settings.elevenlabstts, defaultSettings);
Object.assign(extension_settings.elevenlabstts, defaultSettings)
}
$('#elevenlabs_api_key').val(extension_settings.elevenlabstts.elevenlabsApiKey);
$('#elevenlabs_voice_map').val(extension_settings.elevenlabstts.elevenlabsVoiceMap);
$('#elevenlabs_enabled').prop('checked', extension_settings.elevenlabstts.enabled);
$('#elevenlabs_api_key').val(
extension_settings.elevenlabstts.elevenlabsApiKey
)
$('#elevenlabs_voice_map').val(
extension_settings.elevenlabstts.elevenlabsVoiceMap
)
$('#elevenlabs_enabled').prop(
'checked',
extension_settings.elevenlabstts.enabled
)
onElevenlabsApplyClick()
}
const defaultSettings = {
elevenlabsApiKey: "",
elevenlabsVoiceMap: "",
elevenlabsApiKey: '',
elevenlabsVoiceMap: '',
elevenlabsEnabed: false
};
}
function setElevenLabsStatus(status, success) {
$('#elevenlabs_status').text(status)
if (success) {
$("#elevenlabs_status").removeAttr("style");
$('#elevenlabs_status').removeAttr('style')
} else {
$('#elevenlabs_status').css('color', 'red');
$('#elevenlabs_status').css('color', 'red')
}
}
async function updateApiKey() {
const context = getContext();
const value = $('#elevenlabs_api_key').val();
const context = getContext()
const value = $('#elevenlabs_api_key').val()
// Using this call to validate API key
API_KEY = String(value)
await fetchTtsVoiceIds().catch((error => {
API_KEY = null
ttsProvider.API_KEY = String(value)
await ttsProvider.fetchTtsVoiceIds().catch(error => {
ttsProvider.API_KEY = null
throw `ElevenLabs TTS API key invalid`
}))
})
extension_settings.elevenlabstts.elevenlabsApiKey = String(value);
console.debug(`Saved new API_KEY: ${value}`);
saveSettingsDebounced();
extension_settings.elevenlabstts.elevenlabsApiKey = String(value)
console.debug(`Saved new API_KEY: ${value}`)
saveSettingsDebounced()
}
function parseVoiceMap(voiceMapString) {
let parsedVoiceMap = {}
for (const [charName, voiceId] of voiceMapString.split(",").map(s => s.split(":"))) {
for (const [charName, voiceId] of voiceMapString
.split(',')
.map(s => s.split(':'))) {
if (charName && voiceId) {
parsedVoiceMap[charName.trim()] = voiceId.trim();
parsedVoiceMap[charName.trim()] = voiceId.trim()
}
}
return parsedVoiceMap
@ -377,24 +302,26 @@ function parseVoiceMap(voiceMapString) {
async function getTtsVoice(name) {
// We're caching the list of voice_ids. This might cause trouble if the user creates a new voice without restarting
if (elevenlabsTtsVoices.length == 0) {
elevenlabsTtsVoices = await fetchTtsVoiceIds();
elevenlabsTtsVoices = await ttsProvider.fetchTtsVoiceIds()
}
const match = elevenlabsTtsVoices.filter((elevenVoice) => elevenVoice.name == name)[0];
const match = elevenlabsTtsVoices.filter(
elevenVoice => elevenVoice.name == name
)[0]
if (!match) {
throw `TTS Voice name ${name} not found in ElevenLabs account`;
throw `TTS Voice name ${name} not found in ElevenLabs account`
}
return match;
return match
}
async function voicemapIsValid(parsedVoiceMap) {
let valid = true
for (const characterName in parsedVoiceMap) {
const parsedVoiceName = parsedVoiceMap[characterName];
const parsedVoiceName = parsedVoiceMap[characterName]
try {
await getTtsVoice(parsedVoiceName);
await getTtsVoice(parsedVoiceName)
} catch (error) {
console.error(error)
valid = false;
valid = false
}
}
return valid
@ -402,19 +329,19 @@ async function voicemapIsValid(parsedVoiceMap) {
async function updateVoiceMap() {
let isValidResult = false
const context = getContext();
const context = getContext()
// console.debug("onElevenlabsVoiceMapSubmit");
const value = $('#elevenlabs_voice_map').val();
const parsedVoiceMap = parseVoiceMap(value);
isValidResult = await voicemapIsValid(parsedVoiceMap);
const value = $('#elevenlabs_voice_map').val()
const parsedVoiceMap = parseVoiceMap(value)
isValidResult = await voicemapIsValid(parsedVoiceMap)
if (isValidResult) {
extension_settings.elevenlabstts.elevenlabsVoiceMap = String(value);
extension_settings.elevenlabstts.elevenlabsVoiceMap = String(value)
context.elevenlabsVoiceMap = String(value)
voiceMap = parsedVoiceMap
console.debug(`Saved new voiceMap: ${value}`)
saveSettingsDebounced();
saveSettingsDebounced()
} else {
throw "Voice map is invalid, check console for errors"
throw 'Voice map is invalid, check console for errors'
}
}
@ -422,23 +349,27 @@ function onElevenlabsApplyClick() {
Promise.all([updateApiKey(), updateVoiceMap()])
.then(([result1, result2]) => {
updateUiAudioPlayState()
setElevenLabsStatus("Successfully applied settings", true)
setElevenLabsStatus('Successfully applied settings', true)
})
.catch((error) => {
.catch(error => {
setElevenLabsStatus(error, false)
});
})
}
function onElevenlabsEnableClick() {
extension_settings.elevenlabstts.enabled = $("#elevenlabs_enabled").is(':checked');
extension_settings.elevenlabstts.enabled = $('#elevenlabs_enabled').is(
':checked'
)
updateUiAudioPlayState()
saveSettingsDebounced();
saveSettingsDebounced()
}
function updateUiAudioPlayState() {
if (extension_settings.elevenlabstts.enabled == true) {
audioControl.style.display = 'flex'
const img = !audioElement.paused ? "fa-solid fa-circle-pause" : "fa-solid fa-circle-play"
const img = !audioElement.paused
? 'fa-solid fa-circle-pause'
: 'fa-solid fa-circle-play'
audioControl.className = img
} else {
audioControl.style.display = 'none'
@ -452,9 +383,9 @@ function onAudioControlClicked() {
function addAudioControl() {
$('#send_but_sheld').prepend('<div id="tts_media_control"/>')
$('#tts_media_control').on('click', onAudioControlClicked)
audioControl = document.getElementById('tts_media_control');
updateUiAudioPlayState();
$('#send_but_sheld').on('click', onAudioControlClicked)
audioControl = document.getElementById('tts_media_control')
updateUiAudioPlayState()
}
$(document).ready(function () {
@ -487,14 +418,14 @@ $(document).ready(function () {
</div>
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#elevenlabs_apply').on('click', onElevenlabsApplyClick);
$('#elevenlabs_enabled').on('click', onElevenlabsEnableClick);
$('#elevenlabs_voices').on('click', onElevenlabsVoicesClick);
`
$('#extensions_settings').append(settingsHtml)
$('#elevenlabs_apply').on('click', onElevenlabsApplyClick)
$('#elevenlabs_enabled').on('click', onElevenlabsEnableClick)
$('#elevenlabs_voices').on('click', onElevenlabsVoicesClick)
}
addAudioControl();
addExtensionControls();
loadSettings();
setInterval(moduleWorker, UPDATE_INTERVAL);
});
addAudioControl()
addExtensionControls()
loadSettings()
setInterval(moduleWorker, UPDATE_INTERVAL)
})

View File

@ -266,7 +266,6 @@ async function getSpritesList(name) {
}
async function getExpressionsList() {
console.log('getting expressions list');
// get something for offline mode (default images)
if (!modules.includes('classify')) {
return DEFAULT_EXPRESSIONS;

View File

@ -214,7 +214,9 @@ function printGroups() {
const template = $("#group_list_template .group_select").clone();
template.data("id", group.id);
template.attr("grid", group.id);
template.find(".ch_name").text(group.name);
template.find(".ch_name").html(group.name);
group.fav ? template.find(".group_fav_icon").show() : template.find(".group_fav_icon").hide();
template.find(".ch_fav").val(group.fav);
$("#rm_print_characters_block").prepend(template);
updateGroupAvatar(group);
}
@ -695,7 +697,6 @@ async function reorderGroupMember(chat_id, groupMember, direction) {
function select_group_chats(chat_id, skipAnimation) {
const group = chat_id && groups.find((x) => x.id == chat_id);
const groupName = group?.name ?? "";
$("#rm_group_chat_name").val(groupName);
$("#rm_group_chat_name").off();
$("#rm_group_chat_name").on("input", async function () {
@ -753,6 +754,7 @@ function select_group_chats(chat_id, skipAnimation) {
const groupHasMembers = !!$("#rm_group_members").children().length;
$("#rm_group_submit").prop("disabled", !groupHasMembers);
$("#rm_group_allow_self_responses").prop("checked", group && group.allow_self_responses);
$("#rm_group_fav").prop("checked", group && group.fav);
// bottom buttons
if (chat_id) {
@ -774,11 +776,22 @@ function select_group_chats(chat_id, skipAnimation) {
callPopup("<h3>Delete the group?</h3>", "del_group");
});
$("#rm_group_fav").off();
$("#rm_group_fav").on("input", async function(){
if (group) {
let _thisGroup = groups.find((x) => x.id == chat_id);
const value = $(this).prop("checked");
_thisGroup.fav = value;
await editGroup(chat_id);
}
});
$("#rm_group_allow_self_responses").off();
$("#rm_group_allow_self_responses").on("input", async function () {
if (group) {
let _thisGroup = groups.find((x) => x.id == chat_id);
const value = $(this).prop("checked");
group.allow_self_responses = value;
_thisGroup.allow_self_responses = value;
await editGroup(chat_id);
}
});
@ -829,6 +842,9 @@ $(document).ready(() => {
updateChatMetadata({}, true);
chat.length = 0;
await getGroupChat(id);
//to avoid the filter being lit up yellow and left at true while the list of character and group reseted.
$("#filter_by_fav").removeClass("fav_on");
filterByFav = false;
}
select_group_chats(id);
@ -852,6 +868,7 @@ $(document).ready(() => {
$("#rm_group_submit").click(async function () {
let name = $("#rm_group_chat_name").val();
let allow_self_responses = !!$("#rm_group_allow_self_responses").prop("checked");
let fav = $("#rm_group_fav").prop("checked");
let activation_strategy = $('input[name="rm_group_activation_strategy"]:checked').val() ?? group_activation_strategy.NATURAL;
const members = $("#rm_group_members .group_member")
.map((_, x) => $(x).data("id"))
@ -877,6 +894,7 @@ $(document).ready(() => {
allow_self_responses: allow_self_responses,
activation_strategy: activation_strategy,
chat_metadata: {},
fav: fav,
}),
});

View File

@ -909,6 +909,9 @@ select option:not(:checked) {
cursor: not-allowed;
}
.fav_on {
color: #ffff00 !important;
}
#api_url_text,
#textgenerationwebui_api_url_text {
@ -1138,6 +1141,17 @@ input[type=search]:focus::-webkit-search-cancel-button {
margin-bottom: 4px;
}
#fav_chara_wrap{
display: flex;
margin: 5px 0px;
}
#fav_chara {
border: none;
font-size: var(--mainFontSize);
display: flex;
}
#description_div {
position: relative;
}
@ -2444,6 +2458,9 @@ h5 {
overflow: hidden;
text-overflow: ellipsis;
width: calc(100% - 110px);
display: flex;
align-items: center;
gap: 5px;
}
/* Rules for icon display */
@ -2484,12 +2501,15 @@ h5 {
}
.group_select .ch_name {
flex-grow: 1;
max-width: calc(100% - 100px);
overflow: hidden;
text-overflow: ellipsis;
}
.group_select .group_fav_icon{
margin-left: 5px;
}
#typing_indicator_template {
display: none !important;
}

154
server.js
View File

@ -1,3 +1,27 @@
#!/usr/bin/env node
const process = require('process')
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const cliArguments = yargs(hideBin(process.argv))
.option('ssl', {
type: 'boolean',
default: 'false',
describe: 'Enables SSL'
}).option('certPath', {
type: 'string',
default: 'certs/cert.pem',
describe: 'Path to your certificate file.'
}).option('keyPath', {
type: 'string',
default: 'certs/privkey.pem',
describe: 'Path to your private key file.'
}).argv;
// change all relative paths
process.chdir(__dirname)
const express = require('express');
const compression = require('compression');
const app = express();
@ -9,6 +33,7 @@ const open = require('open');
const rimraf = require("rimraf");
const multer = require("multer");
const http = require("http");
const https = require('https');
//const PNG = require('pngjs').PNG;
const extract = require('png-chunks-extract');
@ -29,10 +54,10 @@ const ExifReader = require('exifreader');
const exif = require('piexifjs');
const webp = require('webp-converter');
const config = require(path.join(process.cwd(), './config.conf'));
const config = require(path.join(__dirname, './config.conf'));
const server_port = process.env.SILLY_TAVERN_PORT || config.port;
const whitelistPath = path.join(process.cwd(), "./whitelist.txt");
const whitelistPath = path.join(__dirname, "./whitelist.txt");
let whitelist = config.whitelist;
if (fs.existsSync(whitelistPath)) {
@ -43,7 +68,7 @@ if (fs.existsSync(whitelistPath)) {
}
const whitelistMode = config.whitelistMode;
const autorun = config.autorun;
const autorun = config.autorun && !cliArguments.ssl;
const enableExtensions = config.enableExtensions;
const listen = config.listen;
@ -183,8 +208,8 @@ app.use(function (req, res, next) { //Security
//clientIp = req.connection.remoteAddress.split(':').pop();
if (whitelistMode === true && !whitelist.includes(clientIp)) {
console.log('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.conf in root of TavernAI folder.\n');
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.conf in root of TavernAI folder.');
console.log('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.conf in root of SillyTavern folder.\n');
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.conf in root of SillyTavern folder.');
}
next();
});
@ -211,7 +236,7 @@ app.use((req, res, next) => {
app.use(express.static(__dirname + "/public", { refresh: true }));
app.use('/backgrounds', (req, res) => {
const filePath = decodeURIComponent(path.join(process.cwd(), 'public/backgrounds', req.url.replace(/%20/g, ' ')));
const filePath = decodeURIComponent(path.join(__dirname, 'public/backgrounds', req.url.replace(/%20/g, ' ')));
fs.readFile(filePath, (err, data) => {
if (err) {
res.status(404).send('File not found');
@ -223,7 +248,7 @@ app.use('/backgrounds', (req, res) => {
});
app.use('/characters', (req, res) => {
const filePath = decodeURIComponent(path.join(process.cwd(), charactersPath, req.url.replace(/%20/g, ' ')));
const filePath = decodeURIComponent(path.join(__dirname, charactersPath, req.url.replace(/%20/g, ' ')));
fs.readFile(filePath, (err, data) => {
if (err) {
res.status(404).send('File not found');
@ -368,7 +393,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
if (!!request.header('X-Response-Streaming')) {
const fn_index = Number(request.header('X-Gradio-Streaming-Function'));
let isStreamingStopped = false;
request.socket.on('close', function() {
request.socket.on('close', function () {
isStreamingStopped = true;
});
@ -675,7 +700,7 @@ function checkServer() {
//***************** Main functions
function charaFormatData(data) {
var char = { "name": data.ch_name, "description": data.description, "personality": data.personality, "first_mes": data.first_mes, "avatar": 'none', "chat": data.ch_name + ' - ' + humanizedISO8601DateTime(), "mes_example": data.mes_example, "scenario": data.scenario, "create_date": humanizedISO8601DateTime(), "talkativeness": data.talkativeness };
var char = { "name": data.ch_name, "description": data.description, "personality": data.personality, "first_mes": data.first_mes, "avatar": 'none', "chat": data.ch_name + ' - ' + humanizedISO8601DateTime(), "mes_example": data.mes_example, "scenario": data.scenario, "create_date": humanizedISO8601DateTime(), "talkativeness": data.talkativeness, "fav": data.fav};
return char;
}
app.post("/createcharacter", urlencodedParser, function (request, response) {
@ -729,10 +754,8 @@ app.post("/editcharacter", urlencodedParser, async function (request, response)
var char = charaFormatData(request.body);//{"name": request.body.ch_name, "description": request.body.description, "personality": request.body.personality, "first_mes": request.body.first_mes, "avatar": request.body.avatar_url, "chat": request.body.chat, "last_mes": request.body.last_mes, "mes_example": ''};
char.chat = request.body.chat;
char.create_date = request.body.create_date;
char = JSON.stringify(char);
let target_img = (request.body.avatar_url).replace('.png', '');
try {
if (!filedata) {
@ -1598,18 +1621,18 @@ app.post("/importchat", urlencodedParser, function (request, response) {
const errors = [];
newChats.forEach(chat => fs.writeFile(
chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + ' imported.jsonl',
chat.map(JSON.stringify).join('\n'),
'utf8',
(err) => err ?? errors.push(err)
)
chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + ' imported.jsonl',
chat.map(JSON.stringify).join('\n'),
'utf8',
(err) => err ?? errors.push(err)
)
);
if (0 < errors.length) {
response.send('Errors occurred while writing character files. Errors: ' + JSON.stringify(errors));
}
response.send({res: true});
response.send({ res: true });
} else {
response.send({ error: true });
}
@ -1755,6 +1778,7 @@ app.post('/creategroup', jsonParser, (request, response) => {
allow_self_responses: !!request.body.allow_self_responses,
activation_strategy: request.body.activation_strategy ?? 0,
chat_metadata: request.body.chat_metadata ?? {},
fav: request.body.fav,
};
const pathToFile = path.join(directories.groups, `${id}.json`);
const fileData = JSON.stringify(chatMetadata);
@ -1771,7 +1795,6 @@ app.post('/editgroup', jsonParser, (request, response) => {
if (!request.body || !request.body.id) {
return response.sendStatus(400);
}
const id = request.body.id;
const pathToFile = path.join(directories.groups, `${id}.json`);
const fileData = JSON.stringify(request.body);
@ -1840,7 +1863,7 @@ app.post('/deletegroup', jsonParser, async (request, response) => {
const POE_DEFAULT_BOT = 'a2';
async function getPoeClient(token, useCache=false) {
async function getPoeClient(token, useCache = false) {
let client = new poe.Client(false, useCache);
await client.init(token);
return client;
@ -1906,7 +1929,7 @@ app.post('/generate_poe', jsonParser, async (request, response) => {
if (streaming) {
let isStreamingStopped = false;
request.socket.on('close', function() {
request.socket.on('close', function () {
isStreamingStopped = true;
client.abortController.abort();
});
@ -1972,17 +1995,17 @@ app.get('/get_sprites', jsonParser, function (request, response) {
try {
if (fs.existsSync(spritesPath) && fs.statSync(spritesPath).isDirectory()) {
sprites = fs.readdirSync(spritesPath)
.filter(file => {
const mimeType = mime.lookup(file);
return mimeType && mimeType.startsWith('image/');
})
.map((file) => {
const pathToSprite = path.join(spritesPath, file);
return {
label: path.parse(pathToSprite).name.toLowerCase(),
path: `/characters/${name}/${file}`,
};
});
.filter(file => {
const mimeType = mime.lookup(file);
return mimeType && mimeType.startsWith('image/');
})
.map((file) => {
const pathToSprite = path.join(spritesPath, file);
return {
label: path.parse(pathToSprite).name.toLowerCase(),
path: `/characters/${name}/${file}`,
};
});
}
}
catch (err) {
@ -2152,12 +2175,42 @@ app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_
});
});
// Shamelessly stolen from Agnai
app.post("/openai_usage", jsonParser, async function (_, response) {
if (!request.body) return response.sendStatus(400);
const key = request.body.key;
const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${key}`,
};
const date = new Date();
date.setDate(1);
const start_date = date.toISOString().slice(0, 10);
date.setMonth(date.getMonth() + 1);
const end_date = date.toISOString().slice(0, 10);
try {
const res = await getAsync(
`${api_url}/dashboard/billing/usage?start_date=${start_date}&end_date=${end_date}`,
{ headers },
);
return response.send(res);
}
catch {
return response.sendStatus(400);
}
});
app.post("/generate_openai", jsonParser, function (request, response_generate_openai) {
if (!request.body) return response_generate_openai.sendStatus(400);
const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
const controller = new AbortController();
request.socket.on('close', function() {
request.socket.on('close', function () {
controller.abort();
});
@ -2319,23 +2372,46 @@ function getAsync(url, args) {
}
// ** END **
app.listen(server_port, (listen ? '0.0.0.0' : '127.0.0.1'), async function () {
const tavernUrl = new URL(
(cliArguments.ssl ? 'https://' : 'http://') +
(listen ? '0.0.0.0' : '127.0.0.1') +
(':' + server_port)
);
const setupTasks = async function () {
ensurePublicDirectoriesExist();
await ensureThumbnailCache();
// Colab users could run the embedded tool
if (!is_colab) {
await convertWebp();
}
if (!is_colab) await convertWebp();
console.log('Launching...');
if (autorun) open('http://127.0.0.1:' + server_port);
console.log('TavernAI started: http://127.0.0.1:' + server_port);
if (autorun) open(tavernUrl);
console.log('SillyTavern started: ' + tavernUrl);
if (fs.existsSync('public/characters/update.txt') && !is_colab) {
convertStage1();
}
}
});
if (true === cliArguments.ssl)
https.createServer(
{
cert: fs.readFileSync(cliArguments.certPath),
key: fs.readFileSync(cliArguments.keyPath)
}, app)
.listen(
tavernUrl.port,
tavernUrl.hostname,
setupTasks
);
else
http.createServer(app).listen(
tavernUrl.port,
tavernUrl.hostname,
setupTasks
);
//#####################CONVERTING IN NEW FORMAT########################