diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 000000000..be97e87e7 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -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}} diff --git a/.gitignore b/.gitignore index 2c12a10ee..a35d5ddc5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ config.conf public/settings.json /thumbnails whitelist.txt +.vscode diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..3311d99ac --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +node_modules/ +/uploads/ +.DS_Store +/thumbnails diff --git a/package-lock.json b/package-lock.json index bb3a10080..135eddfaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index 4f83820ae..a5138b80e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/index.html b/public/index.html index 09cfe0d9e..e8259a8c5 100644 --- a/public/index.html +++ b/public/index.html @@ -1416,13 +1416,27 @@

-
- Description - - ? - +
+
+ +
+
+
+
-
+
+ Description + + ? + + +
@@ -1459,6 +1473,10 @@
+
+ +
@@ -1533,6 +1553,7 @@ +
diff --git a/public/notes/1.html b/public/notes/1.html index 90a0aff5d..70915ec7d 100644 --- a/public/notes/1.html +++ b/public/notes/1.html @@ -1,47 +1,47 @@ - SillyTavern - Note - Character Descriptions - - - - - - + Character Descriptions + + + + -
-
-

Character description

-

- Used to add the character description and the rest that the AI should know.

- 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.

- Usually it all takes 200-350 tokens. -

-

Methods and format

-

- 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.

- The entire description should be in one line without hyphenation.

- For example:

- - 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... - -

+
+
+

Character description

+

+ Used to add the character description and the rest that the AI should know.

+ 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.

+ Usually it all takes 200-350 tokens. +

+

Methods and format

+

+ 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.

+ The entire description should be in one line without hyphenation.

+ For example:

+ + 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... + +

-

- But that the AI would be less confused the best way is to use the W++ format.

- Details here: Pro-Tips -

-
-

- A list of tags that are replaced when sending to generate:

- {{user}} and <USER> are replaced by the User's Name
- {{char}} and <BOT> are replaced by the Character's Name -

-
-
+

+ But that the AI would be less confused the best way is to use the W++ format.

+ Details here: Pro-Tips +

+
+

+ A list of tags that are replaced when sending to generate:

+ {{user}} and <USER> are replaced by the User's Name
+ {{char}} and <BOT> are replaced by the Character's Name +

+
+
\ No newline at end of file diff --git a/public/notes/10.html b/public/notes/10.html index af5027b8d..c8f4b5042 100644 --- a/public/notes/10.html +++ b/public/notes/10.html @@ -1,13 +1,11 @@ - SillyTavern - Note - Import Chat + Import Chat - - - + diff --git a/public/notes/11.html b/public/notes/11.html index 509b629ce..5b32dd06f 100644 --- a/public/notes/11.html +++ b/public/notes/11.html @@ -1,13 +1,11 @@ - SillyTavern - Note - Example Dialogues + Example Dialogues - - - + diff --git a/public/notes/12.html b/public/notes/12.html index cbe00f799..b30b40b14 100644 --- a/public/notes/12.html +++ b/public/notes/12.html @@ -1,13 +1,11 @@ - SillyTavern - Note - Scenario + Scenario - - - + diff --git a/public/notes/13.html b/public/notes/13.html index c1a43ea2e..ba4313f8e 100644 --- a/public/notes/13.html +++ b/public/notes/13.html @@ -5,9 +5,7 @@ - - - + @@ -15,15 +13,20 @@

World Info

World Info enhances AI's understanding of the details in your world.

-

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.

-

The SillyTavern engine activates and seamlessly integrates the appropriate lore into the prompt, providing background information to the AI.

-

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.

+

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.

+

The SillyTavern engine activates and seamlessly integrates the appropriate lore into the prompt, + providing background information to the AI.

+

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.

Pro Tips

diff --git a/public/notes/13_1.html b/public/notes/13_1.html index 209d61107..8a5b860e1 100644 --- a/public/notes/13_1.html +++ b/public/notes/13_1.html @@ -5,9 +5,7 @@ - - - + diff --git a/public/notes/13_2.html b/public/notes/13_2.html index fa44ded3a..af9fa57c7 100644 --- a/public/notes/13_2.html +++ b/public/notes/13_2.html @@ -5,9 +5,7 @@ - - - + diff --git a/public/notes/13_3.html b/public/notes/13_3.html index 21d8d9f4d..36c19d6c5 100644 --- a/public/notes/13_3.html +++ b/public/notes/13_3.html @@ -5,9 +5,7 @@ - - - + diff --git a/public/notes/14.html b/public/notes/14.html index 32f98ad4c..35b430d6d 100644 --- a/public/notes/14.html +++ b/public/notes/14.html @@ -4,10 +4,8 @@ - - - - + +
diff --git a/public/notes/15.html b/public/notes/15.html new file mode 100644 index 000000000..613182a8c --- /dev/null +++ b/public/notes/15.html @@ -0,0 +1,22 @@ + + + + Favorite Character + + + + + + + +
+
+

Favorite Character

+

+ Mark character as favorite to quickly filter on the side menu bar by pressing the star button. +

+
+
+ + + \ No newline at end of file diff --git a/public/notes/2.html b/public/notes/2.html index 1db4523d3..a5419fda8 100644 --- a/public/notes/2.html +++ b/public/notes/2.html @@ -1,13 +1,11 @@ - SillyTavern - Note - Personality Summary + Personality Summary - - - + @@ -15,7 +13,8 @@

Personality summary

- 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.

Example: diff --git a/public/notes/3.html b/public/notes/3.html index 48b28b466..e55ed5300 100644 --- a/public/notes/3.html +++ b/public/notes/3.html @@ -1,40 +1,40 @@ - SillyTavern - Note - First Message - - - - - - + First Message + + + + -
-
-

First message

-

- The First Message is an important thing that sets exactly how and in what style the character will communicate.

- 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.

- You can also use asterisks ** to describe the character's actions. -

+
+
+

First message

+

+ The First Message is an important thing that sets exactly how and in what style the character will + communicate.

+ 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.

+ You can also use asterisks ** to describe the character's actions. +

- For example: -

- + For example: +

+ *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... -
-
-

- A list of tags that are replaced when sending to generate:

- {{user}} and <USER> are replaced by the User's Name
- {{char}} and <BOT> are replaced by the Character's Name -

-
-
+
+
+

+ A list of tags that are replaced when sending to generate:

+ {{user}} and <USER> are replaced by the User's Name
+ {{char}} and <BOT> are replaced by the Character's Name +

+
+
\ No newline at end of file diff --git a/public/notes/4.html b/public/notes/4.html index 99868b65d..244a4f08c 100644 --- a/public/notes/4.html +++ b/public/notes/4.html @@ -1,13 +1,11 @@ - SillyTavern - Note - KobolAI Settings + KoboldAI Settings - - - + diff --git a/public/notes/6.html b/public/notes/6.html index e32cb5188..2e54b4a76 100644 --- a/public/notes/6.html +++ b/public/notes/6.html @@ -1,36 +1,37 @@ - SillyTavern - Note - Novel AI API Key - - - - - - + NovelAI API Key + + + + -
-
-

Finding your NAI API key

-

To get a NovelAI API key, follow these instructions:

-

- 1. Go to the NovelAI website and Login.

+

+
+

Finding your NAI API key

+

To get a NovelAI API key, follow these instructions:

+

+ 1. Go to the NovelAI website and Login.

- 2. Create a new story, or open an existing story.

+ 2. Create a new story, or open an existing story.

- 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.)

+ 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.)

- 4. Generate something. You should see two requests to api.novelai.net/ai/generate-stream, which might look something like this:

-

- 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:

-

- The long string (after "Bearer", not including it) is your API key.

- * Proxies and Cloudflare-type services may interfere with connection. -

-
-
+ 4. Generate something. You should see two requests to api.novelai.net/ai/generate-stream, which might + look something like this:

+

+ 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:

+

+ The long string (after "Bearer", not including it) is your API key.

+ * Proxies and Cloudflare-type services may interfere with connection. +

+
+
\ No newline at end of file diff --git a/public/notes/7.html b/public/notes/7.html index 1ddbaa5b5..984958844 100644 --- a/public/notes/7.html +++ b/public/notes/7.html @@ -1,43 +1,43 @@ - SillyTavern - Note - NovelAI Settings - - - - - - + NovelAI Settings + + + + -
-
-

NovelAI settings

-

- The files with the settings are here (SillyTavern\public\NovelAI Settings).
- You can also manually add your own settings files. -

-

Temperature

-

- Value from 0.1 to 2.0.
- Lower value - the answers are more logical, but less creative.
- Higher value - the answers are more creative, but less logical. -

+
+
+

NovelAI settings

+

+ The files with the settings are here (SillyTavern\public\NovelAI Settings).
+ You can also manually add your own settings files. +

+

Temperature

+

+ Value from 0.1 to 2.0.
+ Lower value - the answers are more logical, but less creative.
+ Higher value - the answers are more creative, but less logical. +

-

Repetition penalty

-

- Repetition penalty is responsible for the penalty of repeated words.
- If the character is fixated on something or repeats the same phrase, then increasing this parameter will fix it.
- It is not recommended to increase this parameter too much for the chat format, as it may break this format.
- The standard value for chat is approximately 1.0 - 1.05 -

-

Repetition penalty range

-

- The range of influence of Repetition penalty in tokens. -

-
-
+

Repetition penalty

+

+ Repetition penalty is responsible for the penalty of repeated words.
+ If the character is fixated on something or repeats the same phrase, then increasing this parameter will + fix it.
+ It is not recommended to increase this parameter too much for the chat format, as it may break this + format.
+ The standard value for chat is approximately 1.0 - 1.05 +

+

Repetition penalty range

+

+ The range of influence of Repetition penalty in tokens. +

+
+
\ No newline at end of file diff --git a/public/notes/8.html b/public/notes/8.html index b4c7cdbc6..25fe1a918 100644 --- a/public/notes/8.html +++ b/public/notes/8.html @@ -1,13 +1,11 @@ - SillyTavern - Note - NovelAI Models + NovelAI Models - - - + diff --git a/public/notes/9.html b/public/notes/9.html index 4317de558..2836699af 100644 --- a/public/notes/9.html +++ b/public/notes/9.html @@ -1,49 +1,53 @@ - SillyTavern - Note - Anchors - - - - - - + Anchors + + + + -
-
-

Anchors

-

- Anchors are used to increase the length of messages.
- There are two types of anchors: Character Anchor and Style Anchor -

-

- Character Anchor - affects the character played by the AI by motivating it to write longer messages.

- Looks like: - [Elaborate speaker] -

-

- Style Anchor - affects the entire AI model, motivating the AI to write longer messages even when it is not acting as the character.

- Looks like: - [Writing style: very long messages] -

-
-

- 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. -

-

- 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. -

-

- 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. -

-

- When using Pygmalion models these anchors are automatically disabled, since Pygmalion already generates long enough messages. -

-
-
+
+
+

Anchors

+

+ Anchors are used to increase the length of messages.
+ There are two types of anchors: Character Anchor and Style Anchor +

+

+ Character Anchor - affects the character played by the AI by motivating it to write longer + messages.

+ Looks like: + [Elaborate speaker] +

+

+ Style Anchor - affects the entire AI model, motivating the AI to write longer messages even when + it is not acting as the character.

+ Looks like: + [Writing style: very long messages] +

+
+

+ 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. +

+

+ 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. +

+

+ 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. +

+

+ When using Pygmalion models these anchors are automatically disabled, since Pygmalion already + generates long enough messages. +

+
+
\ No newline at end of file diff --git a/public/notes/advanced_formatting.html b/public/notes/advanced_formatting.html index d6687ea7d..12b3e1523 100644 --- a/public/notes/advanced_formatting.html +++ b/public/notes/advanced_formatting.html @@ -5,11 +5,7 @@ - - - + diff --git a/public/notes/group_reply_strategy.html b/public/notes/group_reply_strategy.html index a23a8c58c..c5a40b465 100644 --- a/public/notes/group_reply_strategy.html +++ b/public/notes/group_reply_strategy.html @@ -5,11 +5,7 @@ - - - + diff --git a/public/notes/message_sound.html b/public/notes/message_sound.html index ae2480ab4..d10da1f6a 100644 --- a/public/notes/message_sound.html +++ b/public/notes/message_sound.html @@ -5,9 +5,7 @@ - - - + diff --git a/public/notes/multigen.html b/public/notes/multigen.html index d15ba0a7c..0d462cd6a 100644 --- a/public/notes/multigen.html +++ b/public/notes/multigen.html @@ -5,9 +5,7 @@ - - - + diff --git a/public/notes/oai_api_key.html b/public/notes/oai_api_key.html index 44906ca20..c39bca437 100644 --- a/public/notes/oai_api_key.html +++ b/public/notes/oai_api_key.html @@ -5,11 +5,7 @@ - - - + diff --git a/public/notes/textgen_streaming.html b/public/notes/textgen_streaming.html index 0f9465b57..85fffc742 100644 --- a/public/notes/textgen_streaming.html +++ b/public/notes/textgen_streaming.html @@ -5,11 +5,7 @@ - - - + diff --git a/public/notes/token-limits.html b/public/notes/token-limits.html index 955be8329..687947a3d 100644 --- a/public/notes/token-limits.html +++ b/public/notes/token-limits.html @@ -5,9 +5,7 @@ - - - + diff --git a/public/script.js b/public/script.js index 16598f3ae..50ce81916 100644 --- a/public/script.js +++ b/public/script.js @@ -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() { `
-
${item.name}
+
${item.name} ${item.fav == "true" ? '' : ''}
+
` ); //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") { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index bb9ca07dc..8e6527ee0 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -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; diff --git a/public/scripts/extensions/elevenlabstts/elevenlabs.js b/public/scripts/extensions/elevenlabstts/elevenlabs.js new file mode 100644 index 000000000..e033de663 --- /dev/null +++ b/public/scripts/extensions/elevenlabstts/elevenlabs.js @@ -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 '' + } +} diff --git a/public/scripts/extensions/elevenlabstts/index.js b/public/scripts/extensions/elevenlabstts/index.js index fb58215ac..1b0716ca3 100644 --- a/public/scripts/extensions/elevenlabstts/index.js +++ b/public/scripts/extensions/elevenlabstts/index.js @@ -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 + // 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 += `
${voice.name}
`; - popupText += ``; + popupText += `
${voice.name}
` + popupText += `` } - } - catch { + } catch { popupText = 'Could not load voices list. Check your API key.' } - - callPopup(popupText, 'text'); + callPopup(popupText, 'text') } function completeCurrentAudioJob() { @@ -217,21 +134,21 @@ function completeCurrentAudioJob() { /** * Accepts an HTTP response containing audio/mpeg data, and puts the data as a Blob() on the queue for playback - * @param {*} response + * @param {*} response */ 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('
') - $('#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 () {
- `; - $('#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); -}); \ No newline at end of file + addAudioControl() + addExtensionControls() + loadSettings() + setInterval(moduleWorker, UPDATE_INTERVAL) +}) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 3d6250954..6c01e0624 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -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; diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 12752d1e5..6bc95578d 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -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("

Delete the group?

", "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, }), }); diff --git a/public/style.css b/public/style.css index 73e84a391..e7582e391 100644 --- a/public/style.css +++ b/public/style.css @@ -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; } diff --git a/server.js b/server.js index 7c56516ec..83a134e60 100644 --- a/server.js +++ b/server.js @@ -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('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.'); + 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('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.'); } 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########################