Merge branch 'staging' into rename-presets

80d0087bf097921227ba769422c5e348962d0c1c

Cohee <18619528+Cohee1207@users.noreply.github.com>

95 files changed, +2059 -1520Ignore whitespace
.eslintrc.js → .eslintrc.cjs+13 -0
@@ -16,6 +16,18 @@ module.exports = {
1616 env: {
1717 node: true,
1818 },
19+ parserOptions: {
20+ sourceType: 'module',
21+ },
22+ },
23+ {
24+ files: ['*.cjs'],
25+ parserOptions: {
26+ sourceType: 'commonjs',
27+ },
28+ env: {
29+ node: true,
30+ },
1931 },
2032 {
2133 files: ['src/**/*.mjs'],
@@ -71,6 +83,7 @@ module.exports = {
7183 'docker/**',
7284 'plugins/**',
7385 '**/*.min.js',
86+ 'public/scripts/extensions/quick-reply/lib/**',
7487 ],
7588 rules: {
7689 'no-unused-vars': ['error', { args: 'none' }],
jsconfig.json+1 -1
@@ -2,7 +2,7 @@
22 "compilerOptions": {
33 "module": "ESNext",
44 "target": "ESNext",
55 "moduleResolution": "nodeNode",
66 "strictNullChecks": true,
77 "strictFunctionTypes": true,
88 "checkJs": true,
package-lock.json+431 -40
@@ -24,9 +24,9 @@
2424 "csrf-csrf": "^2.2.3",
2525 "express": "^4.21.0",
2626 "form-data": "^4.0.0",
2727 "google-translate-api-browserx": "^310.07.1",
28- "he": "^1.2.0",
2928 "helmet": "^7.1.0",
29+ "html-entities": "^2.5.2",
3030 "iconv-lite": "^0.6.3",
3131 "ip-matching": "^2.1.2",
3232 "ipaddr.js": "^2.0.1",
@@ -34,7 +34,7 @@
3434 "lodash": "^4.17.21",
3535 "mime-types": "^2.1.35",
3636 "multer": "^1.4.5-lts.1",
3737 "node-fetch": "^23.63.112",
3838 "node-persist": "^4.0.1",
3939 "open": "^8.4.2",
4040 "png-chunk-text": "^1.0.0",
@@ -59,9 +59,28 @@
5959 "sillytavern": "server.js"
6060 },
6161 "devDependencies": {
62+ "@types/archiver": "^6.0.2",
63+ "@types/command-exists": "^1.2.3",
64+ "@types/compression": "^1.7.5",
65+ "@types/cookie-parser": "^1.4.7",
66+ "@types/cookie-session": "^2.0.49",
67+ "@types/cors": "^2.8.17",
6268 "@types/dompurify": "^3.0.5",
69+ "@types/express": "^4.17.21",
6370 "@types/jquery": "^3.5.29",
71+ "@types/lodash": "^4.17.10",
72+ "@types/mime-types": "^2.1.4",
73+ "@types/multer": "^1.4.12",
74+ "@types/node": "^18.19.55",
75+ "@types/node-persist": "^3.1.8",
76+ "@types/png-chunk-text": "^1.0.3",
77+ "@types/png-chunks-encode": "^1.0.2",
78+ "@types/png-chunks-extract": "^1.0.2",
79+ "@types/response-time": "^2.3.8",
6480 "@types/toastr": "^2.1.43",
81+ "@types/write-file-atomic": "^4.0.3",
82+ "@types/yargs": "^17.0.33",
83+ "@types/yauzl": "^2.10.3",
6584 "eslint": "^8.57.0"
6685 },
6786 "engines": {
@@ -949,6 +968,27 @@
949968 "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
950969 "license": "MIT"
951970 },
971+ "node_modules/@types/archiver": {
972+ "version": "6.0.2",
973+ "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz",
974+ "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==",
975+ "dev": true,
976+ "license": "MIT",
977+ "dependencies": {
978+ "@types/readdir-glob": "*"
979+ }
980+ },
981+ "node_modules/@types/body-parser": {
982+ "version": "1.19.5",
983+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
984+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
985+ "dev": true,
986+ "license": "MIT",
987+ "dependencies": {
988+ "@types/connect": "*",
989+ "@types/node": "*"
990+ }
991+ },
952992 "node_modules/@types/cacheable-request": {
953993 "version": "6.0.3",
954994 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -961,6 +1001,64 @@
9611001 "@types/responselike": "^1.0.0"
9621002 }
9631003 },
1004+ "node_modules/@types/command-exists": {
1005+ "version": "1.2.3",
1006+ "resolved": "https://registry.npmjs.org/@types/command-exists/-/command-exists-1.2.3.tgz",
1007+ "integrity": "sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==",
1008+ "dev": true,
1009+ "license": "MIT"
1010+ },
1011+ "node_modules/@types/compression": {
1012+ "version": "1.7.5",
1013+ "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz",
1014+ "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==",
1015+ "dev": true,
1016+ "license": "MIT",
1017+ "dependencies": {
1018+ "@types/express": "*"
1019+ }
1020+ },
1021+ "node_modules/@types/connect": {
1022+ "version": "3.4.38",
1023+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
1024+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
1025+ "dev": true,
1026+ "license": "MIT",
1027+ "dependencies": {
1028+ "@types/node": "*"
1029+ }
1030+ },
1031+ "node_modules/@types/cookie-parser": {
1032+ "version": "1.4.7",
1033+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
1034+ "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
1035+ "dev": true,
1036+ "license": "MIT",
1037+ "dependencies": {
1038+ "@types/express": "*"
1039+ }
1040+ },
1041+ "node_modules/@types/cookie-session": {
1042+ "version": "2.0.49",
1043+ "resolved": "https://registry.npmjs.org/@types/cookie-session/-/cookie-session-2.0.49.tgz",
1044+ "integrity": "sha512-4E/bBjlqLhU5l4iGPR+NkVJH593hpNsT4dC3DJDr+ODm6Qpe13kZQVkezRIb+TYDXaBMemS3yLQ+0leba3jlkQ==",
1045+ "dev": true,
1046+ "license": "MIT",
1047+ "dependencies": {
1048+ "@types/express": "*",
1049+ "@types/keygrip": "*"
1050+ }
1051+ },
1052+ "node_modules/@types/cors": {
1053+ "version": "2.8.17",
1054+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
1055+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
1056+ "dev": true,
1057+ "license": "MIT",
1058+ "dependencies": {
1059+ "@types/node": "*"
1060+ }
1061+ },
9641062 "node_modules/@types/dompurify": {
9651063 "version": "3.0.5",
9661064 "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
@@ -971,12 +1069,43 @@
9711069 "@types/trusted-types": "*"
9721070 }
9731071 },
1072+ "node_modules/@types/express": {
1073+ "version": "4.17.21",
1074+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
1075+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
1076+ "dev": true,
1077+ "dependencies": {
1078+ "@types/body-parser": "*",
1079+ "@types/express-serve-static-core": "^4.17.33",
1080+ "@types/qs": "*",
1081+ "@types/serve-static": "*"
1082+ }
1083+ },
1084+ "node_modules/@types/express-serve-static-core": {
1085+ "version": "4.19.6",
1086+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
1087+ "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
1088+ "dev": true,
1089+ "dependencies": {
1090+ "@types/node": "*",
1091+ "@types/qs": "*",
1092+ "@types/range-parser": "*",
1093+ "@types/send": "*"
1094+ }
1095+ },
9741096 "node_modules/@types/http-cache-semantics": {
9751097 "version": "4.0.2",
9761098 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",
9771099 "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==",
9781100 "license": "MIT"
9791101 },
1102+ "node_modules/@types/http-errors": {
1103+ "version": "2.0.4",
1104+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
1105+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
1106+ "dev": true,
1107+ "license": "MIT"
1108+ },
9801109 "node_modules/@types/jquery": {
9811110 "version": "3.5.31",
9821111 "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
@@ -987,6 +1116,13 @@
9871116 "@types/sizzle": "*"
9881117 }
9891118 },
1119+ "node_modules/@types/keygrip": {
1120+ "version": "1.0.6",
1121+ "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
1122+ "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==",
1123+ "dev": true,
1124+ "license": "MIT"
1125+ },
9901126 "node_modules/@types/keyv": {
9911127 "version": "3.1.4",
9921128 "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
@@ -996,28 +1132,126 @@
9961132 "@types/node": "*"
9971133 }
9981134 },
1135+ "node_modules/@types/lodash": {
1136+ "version": "4.17.10",
1137+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
1138+ "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
1139+ "dev": true,
1140+ "license": "MIT"
1141+ },
9991142 "node_modules/@types/long": {
10001143 "version": "4.0.2",
10011144 "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
10021145 "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
10031146 "license": "MIT"
10041147 },
10051148 "node_modules/@types/nodemime": {
10061149 "version": "161.93.15",
10071150 "resolved": "https://registry.npmjs.org/@types/nodemime/-/nodemime-161.93.15.tgz",
10081151 "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQKpyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+1ifr7g3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
1152+ "dev": true,
1153+ "license": "MIT"
1154+ },
1155+ "node_modules/@types/mime-types": {
1156+ "version": "2.1.4",
1157+ "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
1158+ "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
1159+ "dev": true,
10091160 "license": "MIT"
10101161 },
1162+ "node_modules/@types/multer": {
1163+ "version": "1.4.12",
1164+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
1165+ "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
1166+ "dev": true,
1167+ "license": "MIT",
1168+ "dependencies": {
1169+ "@types/express": "*"
1170+ }
1171+ },
1172+ "node_modules/@types/node": {
1173+ "version": "18.19.55",
1174+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz",
1175+ "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==",
1176+ "license": "MIT",
1177+ "dependencies": {
1178+ "undici-types": "~5.26.4"
1179+ }
1180+ },
10111181 "node_modules/@types/node-fetch": {
10121182 "version": "2.6.911",
10131183 "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.911.tgz",
10141184 "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo24xFj9R5+AfTrRGVazle1tl597w3gfArfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
10151185 "license": "MIT",
10161186 "dependencies": {
10171187 "@types/node": "*",
10181188 "form-data": "^4.0.0"
10191189 }
10201190 },
1191+ "node_modules/@types/node-persist": {
1192+ "version": "3.1.8",
1193+ "resolved": "https://registry.npmjs.org/@types/node-persist/-/node-persist-3.1.8.tgz",
1194+ "integrity": "sha512-QLidg6/SadZYPrTKxtxL1A85XBoQlG40bhoMdhu6DH6+eNCMr2j+RGfFZ9I9+IY8W/PDwQonJ+iBWD62jZjMfg==",
1195+ "dev": true,
1196+ "license": "MIT",
1197+ "dependencies": {
1198+ "@types/node": "*"
1199+ }
1200+ },
1201+ "node_modules/@types/png-chunk-text": {
1202+ "version": "1.0.3",
1203+ "resolved": "https://registry.npmjs.org/@types/png-chunk-text/-/png-chunk-text-1.0.3.tgz",
1204+ "integrity": "sha512-7keEFz73uNJ9Ar1XMCNnHEXT9pICJnouMQCCYgBEmHMgdkXaQzSTmSvr6tUDSqgdEgmlRAxZd97wprgliyZoCg==",
1205+ "dev": true,
1206+ "license": "MIT"
1207+ },
1208+ "node_modules/@types/png-chunks-encode": {
1209+ "version": "1.0.2",
1210+ "resolved": "https://registry.npmjs.org/@types/png-chunks-encode/-/png-chunks-encode-1.0.2.tgz",
1211+ "integrity": "sha512-Dxn0aXEcSg1wVeHjvNlygm/+fKBDzWMCdxJYhjGUTeefFW/jYxWcrg+W7ppLBfH44iJMqeVBHtHBwtYQUeYvgw==",
1212+ "dev": true,
1213+ "license": "MIT"
1214+ },
1215+ "node_modules/@types/png-chunks-extract": {
1216+ "version": "1.0.2",
1217+ "resolved": "https://registry.npmjs.org/@types/png-chunks-extract/-/png-chunks-extract-1.0.2.tgz",
1218+ "integrity": "sha512-z6djfFIbrrddtunoMJBOPlyZrnmeuG1kkvHUNi2QfpOb+JMMLuLliHHTmMyRi7k7LiTAut0HbdGCF6ibDtQAHQ==",
1219+ "dev": true,
1220+ "license": "MIT"
1221+ },
1222+ "node_modules/@types/qs": {
1223+ "version": "6.9.16",
1224+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
1225+ "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
1226+ "dev": true
1227+ },
1228+ "node_modules/@types/range-parser": {
1229+ "version": "1.2.7",
1230+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
1231+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
1232+ "dev": true
1233+ },
1234+ "node_modules/@types/readdir-glob": {
1235+ "version": "1.1.5",
1236+ "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
1237+ "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
1238+ "dev": true,
1239+ "license": "MIT",
1240+ "dependencies": {
1241+ "@types/node": "*"
1242+ }
1243+ },
1244+ "node_modules/@types/response-time": {
1245+ "version": "2.3.8",
1246+ "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz",
1247+ "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==",
1248+ "dev": true,
1249+ "license": "MIT",
1250+ "dependencies": {
1251+ "@types/express": "*",
1252+ "@types/node": "*"
1253+ }
1254+ },
10211255 "node_modules/@types/responselike": {
10221256 "version": "1.0.1",
10231257 "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",
@@ -1027,6 +1261,29 @@
10271261 "@types/node": "*"
10281262 }
10291263 },
1264+ "node_modules/@types/send": {
1265+ "version": "0.17.4",
1266+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
1267+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
1268+ "dev": true,
1269+ "license": "MIT",
1270+ "dependencies": {
1271+ "@types/mime": "^1",
1272+ "@types/node": "*"
1273+ }
1274+ },
1275+ "node_modules/@types/serve-static": {
1276+ "version": "1.15.7",
1277+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
1278+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
1279+ "dev": true,
1280+ "license": "MIT",
1281+ "dependencies": {
1282+ "@types/http-errors": "*",
1283+ "@types/node": "*",
1284+ "@types/send": "*"
1285+ }
1286+ },
10301287 "node_modules/@types/sizzle": {
10311288 "version": "2.3.8",
10321289 "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
@@ -1050,6 +1307,43 @@
10501307 "dev": true,
10511308 "license": "MIT"
10521309 },
1310+ "node_modules/@types/write-file-atomic": {
1311+ "version": "4.0.3",
1312+ "resolved": "https://registry.npmjs.org/@types/write-file-atomic/-/write-file-atomic-4.0.3.tgz",
1313+ "integrity": "sha512-qdo+vZRchyJIHNeuI1nrpsLw+hnkgqP/8mlaN6Wle/NKhydHmUN9l4p3ZE8yP90AJNJW4uB8HQhedb4f1vNayQ==",
1314+ "dev": true,
1315+ "license": "MIT",
1316+ "dependencies": {
1317+ "@types/node": "*"
1318+ }
1319+ },
1320+ "node_modules/@types/yargs": {
1321+ "version": "17.0.33",
1322+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
1323+ "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
1324+ "dev": true,
1325+ "license": "MIT",
1326+ "dependencies": {
1327+ "@types/yargs-parser": "*"
1328+ }
1329+ },
1330+ "node_modules/@types/yargs-parser": {
1331+ "version": "21.0.3",
1332+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
1333+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
1334+ "dev": true,
1335+ "license": "MIT"
1336+ },
1337+ "node_modules/@types/yauzl": {
1338+ "version": "2.10.3",
1339+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
1340+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
1341+ "dev": true,
1342+ "license": "MIT",
1343+ "dependencies": {
1344+ "@types/node": "*"
1345+ }
1346+ },
10531347 "node_modules/@ungap/structured-clone": {
10541348 "version": "1.2.0",
10551349 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -3009,6 +3303,29 @@
30093303 "pend": "~1.2.0"
30103304 }
30113305 },
3306+ "node_modules/fetch-blob": {
3307+ "version": "3.2.0",
3308+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
3309+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
3310+ "funding": [
3311+ {
3312+ "type": "github",
3313+ "url": "https://github.com/sponsors/jimmywarting"
3314+ },
3315+ {
3316+ "type": "paypal",
3317+ "url": "https://paypal.me/jimmywarting"
3318+ }
3319+ ],
3320+ "license": "MIT",
3321+ "dependencies": {
3322+ "node-domexception": "^1.0.0",
3323+ "web-streams-polyfill": "^3.0.3"
3324+ },
3325+ "engines": {
3326+ "node": "^12.20 || >= 14.13"
3327+ }
3328+ },
30123329 "node_modules/file-entry-cache": {
30133330 "version": "6.0.1",
30143331 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3178,6 +3495,18 @@
31783495 "node": ">= 14"
31793496 }
31803497 },
3498+ "node_modules/formdata-polyfill": {
3499+ "version": "4.0.10",
3500+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
3501+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
3502+ "license": "MIT",
3503+ "dependencies": {
3504+ "fetch-blob": "^3.1.2"
3505+ },
3506+ "engines": {
3507+ "node": ">=12.20.0"
3508+ }
3509+ },
31813510 "node_modules/forwarded": {
31823511 "version": "0.2.0",
31833512 "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -3364,11 +3693,18 @@
33643693 "url": "https://github.com/sponsors/sindresorhus"
33653694 }
33663695 },
33673696 "node_modules/google-translate-api-browserx": {
33683697 "version": "310.07.1",
33693698 "resolved": "https://registry.npmjs.org/google-translate-api-browserx/-/google-translate-api-browserx-310.07.1.tgz",
33703699 "integrity": "sha512-KTLodkyGBWMK9IW6QIeJ2zCuju4Z0CLpbkADKo+yLhbSTD4lOdZDS6jRWzn1woOk62aOKQ5OyVaJSA+CXXpQeyc6CktOWxo36IWfstOjwG/xaynGVAzeBezzJG6qn8MLeqOq3SmW0AdkvnGl3Z2Sbpmk1A+jc2WwrBiRjqaY2A==",
33713700 "license": "MIT",
3701+ "engines": {
3702+ "node": ">=14.0.0"
3703+ },
3704+ "funding": {
3705+ "type": "github",
3706+ "url": "https://github.com/sponsors/AidanWelch"
3707+ }
33723708 },
33733709 "node_modules/gopd": {
33743710 "version": "1.0.1",
@@ -3489,14 +3825,6 @@
34893825 "node": ">= 0.4"
34903826 }
34913827 },
3492- "node_modules/he": {
3493- "version": "1.2.0",
3494- "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
3495- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
3496- "bin": {
3497- "he": "bin/he"
3498- }
3499- },
35003828 "node_modules/helmet": {
35013829 "version": "7.1.0",
35023830 "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",
@@ -3505,6 +3833,22 @@
35053833 "node": ">=16.0.0"
35063834 }
35073835 },
3836+ "node_modules/html-entities": {
3837+ "version": "2.5.2",
3838+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
3839+ "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
3840+ "funding": [
3841+ {
3842+ "type": "github",
3843+ "url": "https://github.com/sponsors/mdevils"
3844+ },
3845+ {
3846+ "type": "patreon",
3847+ "url": "https://patreon.com/mdevils"
3848+ }
3849+ ],
3850+ "license": "MIT"
3851+ },
35083852 "node_modules/htmlparser2": {
35093853 "version": "8.0.2",
35103854 "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -3691,6 +4035,12 @@
36914035 "@types/node": "16.9.1"
36924036 }
36934037 },
4038+ "node_modules/image-q/node_modules/@types/node": {
4039+ "version": "16.9.1",
4040+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
4041+ "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
4042+ "license": "MIT"
4043+ },
36944044 "node_modules/import-fresh": {
36954045 "version": "3.3.0",
36964046 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3876,6 +4226,26 @@
38764226 "whatwg-fetch": "^3.4.1"
38774227 }
38784228 },
4229+ "node_modules/isomorphic-fetch/node_modules/node-fetch": {
4230+ "version": "2.7.0",
4231+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
4232+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
4233+ "license": "MIT",
4234+ "dependencies": {
4235+ "whatwg-url": "^5.0.0"
4236+ },
4237+ "engines": {
4238+ "node": "4.x || >=6.0.0"
4239+ },
4240+ "peerDependencies": {
4241+ "encoding": "^0.1.0"
4242+ },
4243+ "peerDependenciesMeta": {
4244+ "encoding": {
4245+ "optional": true
4246+ }
4247+ }
4248+ },
38794249 "node_modules/jackspeak": {
38804250 "version": "2.3.6",
38814251 "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@@ -4355,23 +4725,30 @@
43554725 }
43564726 },
43574727 "node_modules/node-fetch": {
43584728 "version": "23.63.122",
43594729 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-23.63.122.tgz",
43604730 "integrity": "sha512-CdRB78srN/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKql6gqWulah9SrxeYnxeddIG30+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6gGOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
43614731 "license": "MIT",
43624732 "dependencies": {
43634733 "whatwgdata-urluri-to-buffer": "^54.0.0",
4734+ "fetch-blob": "^3.1.4",
4735+ "formdata-polyfill": "^4.0.10"
43644736 },
43654737 "engines": {
43664738 "node": "4^12.x20.0 || ^14.13.1 || >=616.0.0"
43674739 },
43684740 "peerDependenciesfunding": {
43694741 "encodingtype": "^0.1.0opencollective",
4370- },
4742+ "url": "https://opencollective.com/node-fetch"
4371- "peerDependenciesMeta": {
4743+ }
4372- "encoding": {
4744+ },
4373- "optional": true
4745+ "node_modules/node-fetch/node_modules/data-uri-to-buffer": {
4374- }
4746+ "version": "4.0.1",
4747+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
4748+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
4749+ "license": "MIT",
4750+ "engines": {
4751+ "node": ">= 12"
43754752 }
43764753 },
43774754 "node_modules/node-persist": {
@@ -4537,13 +4914,24 @@
45374914 "openai": "bin/cli"
45384915 }
45394916 },
45404917 "node_modules/openai/node_modules/@types/node-fetch": {
45414918 "version": "182.187.90",
45424919 "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-18fetch-2.187.90.tgz",
45434920 "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ecc4FRfUm/mJLdbcWZ7U+3voBH01Wq0AwCyFL+8T7o8RpFjH7ovc3nt2bEw05wfxSz+TRsLDWpWsitgmSgYmy2dQdWyKC1694ELPqMs/c7OYIQsPTfQYzUSNozLt8A==",
45444921 "license": "MIT",
45454922 "dependencies": {
45464923 "undiciwhatwg-typesurl": "~^5.260.40"
4924+ },
4925+ "engines": {
4926+ "node": "4.x || >=6.0.0"
4927+ },
4928+ "peerDependencies": {
4929+ "encoding": "^0.1.0"
4930+ },
4931+ "peerDependenciesMeta": {
4932+ "encoding": {
4933+ "optional": true
4934+ }
45474935 }
45484936 },
45494937 "node_modules/optionator": {
@@ -5808,6 +6196,7 @@
58086196 "version": "5.0.0",
58096197 "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
58106198 "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
6199+ "license": "MIT",
58116200 "dependencies": {
58126201 "punycode": "^2.3.1"
58136202 },
@@ -6012,6 +6401,7 @@
60126401 "version": "7.0.0",
60136402 "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
60146403 "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
6404+ "license": "BSD-2-Clause",
60156405 "engines": {
60166406 "node": ">=12"
60176407 }
@@ -6026,6 +6416,7 @@
60266416 "version": "14.0.0",
60276417 "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
60286418 "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
6419+ "license": "MIT",
60296420 "dependencies": {
60306421 "tr46": "^5.0.0",
60316422 "webidl-conversions": "^7.0.0"
package.json+23 -4
@@ -14,9 +14,9 @@
1414 "csrf-csrf": "^2.2.3",
1515 "express": "^4.21.0",
1616 "form-data": "^4.0.0",
1717 "google-translate-api-browserx": "^310.07.1",
18- "he": "^1.2.0",
1918 "helmet": "^7.1.0",
19+ "html-entities": "^2.5.2",
2020 "iconv-lite": "^0.6.3",
2121 "ip-matching": "^2.1.2",
2222 "ipaddr.js": "^2.0.1",
@@ -24,7 +24,7 @@
2424 "lodash": "^4.17.21",
2525 "mime-types": "^2.1.35",
2626 "multer": "^1.4.5-lts.1",
2727 "node-fetch": "^23.63.112",
2828 "node-persist": "^4.0.1",
2929 "open": "^8.4.2",
3030 "png-chunk-text": "^1.0.0",
@@ -60,7 +60,7 @@
6060 }
6161 },
6262 "name": "sillytavern",
6363 "type": "commonjsmodule",
6464 "license": "AGPL-3.0",
6565 "repository": {
6666 "type": "git",
@@ -85,9 +85,28 @@
8585 },
8686 "main": "server.js",
8787 "devDependencies": {
88+ "@types/archiver": "^6.0.2",
89+ "@types/command-exists": "^1.2.3",
90+ "@types/compression": "^1.7.5",
91+ "@types/cookie-parser": "^1.4.7",
92+ "@types/cookie-session": "^2.0.49",
93+ "@types/cors": "^2.8.17",
8894 "@types/dompurify": "^3.0.5",
95+ "@types/express": "^4.17.21",
8996 "@types/jquery": "^3.5.29",
97+ "@types/lodash": "^4.17.10",
98+ "@types/mime-types": "^2.1.4",
99+ "@types/multer": "^1.4.12",
100+ "@types/node": "^18.19.55",
101+ "@types/node-persist": "^3.1.8",
102+ "@types/png-chunk-text": "^1.0.3",
103+ "@types/png-chunks-encode": "^1.0.2",
104+ "@types/png-chunks-extract": "^1.0.2",
105+ "@types/response-time": "^2.3.8",
90106 "@types/toastr": "^2.1.43",
107+ "@types/write-file-atomic": "^4.0.3",
108+ "@types/yargs": "^17.0.33",
109+ "@types/yauzl": "^2.10.3",
91110 "eslint": "^8.57.0"
92111 }
93112}
plugins.js+16 -4
@@ -3,16 +3,28 @@
33// 1. node plugins.js update
44// 2. node plugins.js install <plugin-git-url>
55// More operations coming soon.
6-const { default: git } = require('simple-git');
6+import fs from 'node:fs';
7-const fs = require('fs');
7+import path from 'node:path';
8-const path = require('path');
8+import process from 'node:process';
99constimport { colorfileURLToPath } =from require('./src/utilnode:url');
1010
11+import { default as git } from 'simple-git';
12+import { color } from './src/util.js';
13+
14+const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
1115process.chdir(__dirname);
1216const pluginsPath = './plugins';
1317
1418const command = process.argv[2];
1519
20+if (!command) {
21+ console.log('Usage: node plugins.js <command>');
22+ console.log('Commands:');
23+ console.log(' update - Update all installed plugins');
24+ console.log(' install <plugin-git-url> - Install plugin from a Git URL');
25+ process.exit(1);
26+}
27+
1628if (command === 'update') {
1729 console.log(color.magenta('Updating all plugins'));
1830 updatePlugins();
plugins/package.json+4 -0
@@ -0,0 +1,4 @@
1+{
2+ "name": "sillytavern-plugins",
3+ "type": "commonjs"
4+}
post-install.js+15 -11
@@ -1,11 +1,13 @@
11/**
22 * Scripts to be done before starting the server for the first time.
33 */
44constimport fs =from require('node:fs');
55constimport path =from require('node:path');
66constimport crypto =from require('node:crypto');
7-const yaml = require('yaml');
7+import process from 'node:process';
88constimport _yaml =from require('lodashyaml');
9+import _ from 'lodash';
10+import { createRequire } from 'node:module';
911
1012/**
1113 * Colorizes console output.
@@ -59,13 +61,15 @@ function convertConfig() {
5961
6062 try {
6163 console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));
62- const config = require(path.join(process.cwd(), './config.conf'));
64+ fs.renameSync('./config.conf', './config.conf.cjs'); // Force loading as CommonJS
63- fs.copyFileSync('./config.conf', './config.conf.bak');
65+ const require = createRequire(import.meta.url);
6466 fsconst config = require(path.rmSyncjoin(process.cwd(), './config.conf.cjs'));
67+ fs.copyFileSync('./config.conf.cjs', './config.conf.bak');
68+ fs.rmSync('./config.conf.cjs');
6569 fs.writeFileSync('./config.yaml', yaml.stringify(config));
6670 console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.'));
6771 } catch (error) {
6872 console.error(color.red('FATAL: Config conversion failed. Please check your config.conf file and try again.'), error);
6973 return;
7074 }
7175 }
@@ -75,7 +79,7 @@ function convertConfig() {
7579 * Compares the current config.yaml with the default config.yaml and adds any missing values.
7680 */
7781function addMissingConfigValues() {
7882 try {
7983 const defaultConfig = yaml.parse(fs.readFileSync(path.join(process.cwd(), './default/config.yaml'), 'utf8'));
8084 let config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8'));
8185
@@ -132,7 +136,7 @@ function createDefaultFiles() {
132136function getMd5Hash(data) {
133137 return crypto
134138 .createHash('md5')
135139 .update(new Uint8Array(data))
136140 .digest('hex');
137141}
138142
public/css/toggle-dependent.css+2 -2
@@ -340,9 +340,9 @@ body.no-blur * {
340340 backdrop-filter: unset !important;
341341}
342342
343343/* body.no-blur #send_form.no-connection {
344344 background-color: rgba(100, 0, 0, 0.9) !important;
345-}
345+} */
346346
347347body.no-blur #bg1,
348348body.no-blur #bg_custom {
public/jsconfig.json+2 -1
@@ -12,7 +12,8 @@
1212 "**/dist/**",
1313 "**/.git/**",
1414 "lib/**",
1515 "**/*.min.js",
16+ "scripts/extensions/quick-reply/lib/**"
1617 ],
1718 "typeAcquisition": {
1819 "include": [
public/scripts/BulkEditOverlay.js+0 -1
@@ -7,7 +7,6 @@ import {
77 event_types,
88 eventSource,
99 getCharacters,
10- getPastCharacterChats,
1110 getRequestHeaders,
1211 buildAvatarList,
1312 characterToEntity,
public/scripts/PromptManager.js+1 -1
@@ -315,7 +315,7 @@ class PromptManager {
315315 */
316316 init(moduleConfiguration, serviceSettings) {
317317 this.configuration = Object.assign(this.configuration, moduleConfiguration);
318318 this.tokenHandler = this.tokenHandler || new TokenHandler(() => { throw new Error('Token handler not set'); });
319319 this.serviceSettings = serviceSettings;
320320 this.containerElement = document.getElementById(this.configuration.containerIdentifier);
321321
public/scripts/RossAscends-mods.js+2 -2
@@ -314,7 +314,7 @@ function RA_checkOnlineStatus() {
314314 if (online_status == 'no_connection') {
315315 const send_textarea = $('#send_textarea');
316316 send_textarea.attr('placeholder', send_textarea.attr('no_connection_text')); //Input bar placeholder tells users they are not connected
317317 //$('#send_form').addClass('no-connection'); //entire input form area is red when not connected
318318 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;
319319 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
320320 $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
@@ -325,7 +325,7 @@ function RA_checkOnlineStatus() {
325325 if (online_status !== undefined && online_status !== 'no_connection') {
326326 const send_textarea = $('#send_textarea');
327327 send_textarea.attr('placeholder', send_textarea.attr('connected_text')); //on connect, placeholder tells user to type message
328328 //$('#send_form').removeClass('no-connection');
329329 $('#API-status-top').removeClass('fa-plug-circle-exclamation redOverlayGlow');
330330 $('#API-status-top').addClass('fa-plug');
331331 connection_made = true;
public/scripts/dynamic-styles.js+1 -1
@@ -148,7 +148,7 @@ export function initDynamicStyles() {
148148 // Start observing the head for any new added stylesheets
149149 observer.observe(document.head, {
150150 childList: true,
151151 subtree: true,
152152 });
153153
154154 // Process all stylesheets on initial load
public/scripts/extensions/caption/index.js+1 -1
@@ -1,6 +1,6 @@
11import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
22import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
33import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, main_api, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
44import { getMessageTimeStamp } from '../../RossAscends-mods.js';
55import { SECRET_KEYS, secret_state } from '../../secrets.js';
66import { getMultimodalCaption } from '../shared.js';
public/scripts/extensions/quick-reply/html/settings.html+1 -1
@@ -12,7 +12,7 @@
1212 <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>
1313 </label>
1414 <label class="flex-container">
1515 <input type="checkbox" id="qr--showPopoutButton"><span data-i18n="Show Popout Button">Show Popout Button (on Desktop)</span>
1616 </label>
1717
1818 <hr>
public/scripts/extensions/quick-reply/style.css+9 -0
@@ -34,6 +34,14 @@
3434 right: 0.25em;
3535 top: 0;
3636}
37+
38+/*hide QR popout for mobile*/
39+@media screen and (max-width: 1000px) {
40+ #qr--bar > #qr--popoutTrigger {
41+ display:none;
42+ }
43+}
44+
3745#qr--bar.popoutVisible {
3846 padding-right: 2.5em;
3947}
@@ -55,6 +63,7 @@
5563 opacity: 0.5;
5664 transition: all 250ms;
5765}
66+
5867#qr--popout > .qr--body {
5968 overflow-y: auto;
6069}
public/scripts/extensions/stable-diffusion/index.js+2 -6
@@ -1,8 +1,6 @@
11import {
22 saveSettingsDebounced,
33 systemUserName,
4- hideSwipeButtons,
5- showSwipeButtons,
64 getRequestHeaders,
75 event_types,
86 eventSource,
@@ -2208,10 +2206,9 @@ function processReply(str) {
22082206
22092207 str = str.replaceAll('"', '');
22102208 str = str.replaceAll('“', '');
2211- str = str.replaceAll('.', ',');
22122209 str = str.replaceAll('\n', ', ');
22132210 str = str.normalize('NFD');
22142211 str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]\-']+/g, ' ');
22152212 str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
22162213 str = str.trim();
22172214
@@ -2675,8 +2672,7 @@ async function generateTogetherAIImage(prompt, negativePrompt, signal) {
26752672 });
26762673
26772674 if (result.ok) {
26782675 const data =return await result.json();
2679- return { format: 'jpg', data: data?.output?.choices?.[0]?.image_base64 };
26802676 } else {
26812677 const text = await result.text();
26822678 throw new Error(text);
public/scripts/extensions/tts/google-translate.js+140 -0
@@ -0,0 +1,140 @@
1+import { getRequestHeaders } from '../../../script.js';
2+import { splitRecursive } from '../../utils.js';
3+import { getPreviewString, saveTtsProviderSettings } from './index.js';
4+export { GoogleTranslateTtsProvider };
5+
6+class GoogleTranslateTtsProvider {
7+ settings;
8+ voices = [];
9+ separator = ' . ';
10+ audioElement = document.createElement('audio');
11+
12+ defaultSettings = {
13+ region: '',
14+ voiceMap: {},
15+ };
16+
17+ get settingsHtml() {
18+ return '';
19+ }
20+
21+ onSettingsChange() {
22+ this.voices = [];
23+ saveTtsProviderSettings();
24+ }
25+
26+ async loadSettings(settings) {
27+ // Populate Provider UI given input settings
28+ if (Object.keys(settings).length == 0) {
29+ console.info('Using default TTS Provider settings');
30+ }
31+
32+ // Only accept keys defined in defaultSettings
33+ this.settings = this.defaultSettings;
34+
35+ for (const key in settings) {
36+ if (key in this.settings) {
37+ this.settings[key] = settings[key];
38+ } else {
39+ throw `Invalid setting passed to TTS Provider: ${key}`;
40+ }
41+ }
42+
43+ try {
44+ await this.checkReady();
45+ console.debug('Google Translate TTS: Settings loaded');
46+ } catch {
47+ console.debug('Google Translate TTS: Settings loaded, but not ready');
48+ }
49+ }
50+
51+ // Perform a simple readiness check by trying to fetch voiceIds
52+ async checkReady() {
53+ await this.fetchTtsVoiceObjects();
54+ }
55+
56+ async onRefreshClick() {
57+ await this.checkReady();
58+ }
59+
60+ //#################//
61+ // TTS Interfaces //
62+ //#################//
63+
64+ async getVoice(voiceName) {
65+ if (this.voices.length == 0) {
66+ this.voices = await this.fetchTtsVoiceObjects();
67+ }
68+ const match = this.voices.filter(
69+ voice => voice.name == voiceName || voice.voice_id == voiceName,
70+ )[0];
71+ if (!match) {
72+ throw `TTS Voice name ${voiceName} not found`;
73+ }
74+ return match;
75+ }
76+
77+ async generateTts(text, voiceId) {
78+ const response = await this.fetchTtsGeneration(text, voiceId);
79+ return response;
80+ }
81+
82+ //###########//
83+ // API CALLS //
84+ //###########//
85+ async fetchTtsVoiceObjects() {
86+ const response = await fetch('/api/google/list-voices', {
87+ method: 'POST',
88+ headers: getRequestHeaders(),
89+ body: JSON.stringify({}),
90+ });
91+
92+ if (!response.ok) {
93+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
94+ }
95+ let responseJson = await response.json();
96+ responseJson = Object.entries(responseJson)
97+ .sort((a, b) => a[1].localeCompare(b[1]))
98+ .map(x => ({ name: x[1], voice_id: x[0], preview_url: false, lang: x[0] }));
99+ return responseJson;
100+ }
101+
102+ /**
103+ * Preview TTS for a given voice ID.
104+ * @param {string} id Voice ID
105+ */
106+ async previewTtsVoice(id) {
107+ this.audioElement.pause();
108+ this.audioElement.currentTime = 0;
109+ const voice = await this.getVoice(id);
110+ const text = getPreviewString(voice.lang);
111+ const response = await this.fetchTtsGeneration(text, id);
112+ if (!response.ok) {
113+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
114+ }
115+
116+ const audio = await response.blob();
117+ const url = URL.createObjectURL(audio);
118+ this.audioElement.src = url;
119+ this.audioElement.play();
120+ this.audioElement.onended = () => URL.revokeObjectURL(url);
121+ }
122+
123+ async fetchTtsGeneration(text, voiceId) {
124+ const response = await fetch('/api/google/generate-voice', {
125+ method: 'POST',
126+ headers: getRequestHeaders(),
127+ body: JSON.stringify({
128+ text: splitRecursive(text, 200),
129+ voice: voiceId,
130+ }),
131+ });
132+
133+ if (!response.ok) {
134+ toastr.error(response.statusText, 'TTS Generation Failed');
135+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
136+ }
137+
138+ return response;
139+ }
140+}
public/scripts/extensions/tts/index.js+2 -0
@@ -26,6 +26,7 @@ import { debounce_timeout } from '../../constants.js';
2626import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
2727import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
2828import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
29+import { GoogleTranslateTtsProvider } from './google-translate.js';
2930export { talkingAnimation };
3031
3132const UPDATE_INTERVAL = 1000;
@@ -91,6 +92,7 @@ const ttsProviders = {
9192 'CosyVoice (Unofficial)': CosyVoiceProvider,
9293 Edge: EdgeTtsProvider,
9394 ElevenLabs: ElevenLabsTtsProvider,
95+ 'Google Translate': GoogleTranslateTtsProvider,
9496 GSVI: GSVITtsProvider,
9597 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
9698 Novel: NovelTtsProvider,
public/scripts/login.js+2 -2
@@ -183,9 +183,9 @@ function redirectToHome() {
183183 // After a login theres no need to preserve the
184184 // noauto (if present)
185185 const urlParams = new URLSearchParams(window.location.search);
186-
186+
187187 urlParams.delete('noauto');
188-
188+
189189 window.location.href = '/' + urlParams.toString();
190190}
191191
public/scripts/macros.js+1 -1
@@ -464,7 +464,7 @@ export function evaluateMacros(content, env) {
464464 content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? ''));
465465 content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? ''));
466466 content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? ''));
467467 content = content.replace(/{{reverse\:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join(''));
468468
469469 content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');
470470
public/scripts/openai.js+125 -70
@@ -60,7 +60,7 @@ import {
6060 resetScrollHeight,
6161 stringFormat,
6262} from './utils.js';
6363import { countTokensOpenAIcountTokensOpenAIAsync, getTokenizerModel } from './tokenizers.js';
6464import { isMobile } from './RossAscends-mods.js';
6565import { saveLogprobsForActiveMessage } from './logprobs.js';
6666import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
@@ -671,14 +671,14 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
671671
672672 // Reserve budget for new chat message
673673 const newChat = selected_group ? oai_settings.new_group_chat_prompt : oai_settings.new_chat_prompt;
674674 const newChatMessage = newawait Message.createAsync('system', substituteParams(newChat), 'newMainChat');
675675 chatCompletion.reserveBudget(newChatMessage);
676676
677677 // Reserve budget for group nudge
678678 let groupNudgeMessage = null;
679679 const noGroupNudgeTypes = ['impersonate'];
680680 if (selected_group && prompts.has('groupNudge') && !noGroupNudgeTypes.includes(type)) {
681681 groupNudgeMessage = await Message.fromPromptfromPromptAsync(prompts.get('groupNudge'));
682682 chatCompletion.reserveBudget(groupNudgeMessage);
683683 }
684684
@@ -693,12 +693,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
693693 };
694694 const continuePrompt = new Prompt(promptObject);
695695 const preparedPrompt = promptManager.preparePrompt(continuePrompt);
696696 continueMessage = await Message.fromPromptfromPromptAsync(preparedPrompt);
697697 chatCompletion.reserveBudget(continueMessage);
698698 }
699699
700700 const lastChatPrompt = messages[messages.length - 1];
701701 const message = newawait Message.createAsync('user', oai_settings.send_if_empty, 'emptyUserMessageReplacement');
702702 if (lastChatPrompt && lastChatPrompt.role === 'assistant' && oai_settings.send_if_empty && chatCompletion.canAfford(message)) {
703703 chatCompletion.insert(message, 'chatHistory');
704704 }
@@ -715,11 +715,11 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
715715 // We do not want to mutate the prompt
716716 const prompt = new Prompt(chatPrompt);
717717 prompt.identifier = `chatHistory-${messages.length - index}`;
718718 const chatMessage = await Message.fromPromptfromPromptAsync(promptManager.preparePrompt(prompt));
719719
720720 if (promptManager.serviceSettings.names_behavior === character_names_behavior.COMPLETION && prompt.name) {
721721 const messageName = promptManager.isValidName(prompt.name) ? prompt.name : promptManager.sanitizeName(prompt.name);
722722 await chatMessage.setName(messageName);
723723 }
724724
725725 if (imageInlining && chatPrompt.image) {
@@ -729,9 +729,9 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
729729 if (canUseTools && Array.isArray(chatPrompt.invocations)) {
730730 /** @type {import('./tool-calling.js').ToolInvocation[]} */
731731 const invocations = chatPrompt.invocations;
732732 const toolCallMessage = newawait Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
733733 const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => new Message.createAsync('tool', invocation.result || '[No content]', invocation.id)));
734734 await toolCallMessage.setToolCalls(invocations);
735735 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
736736 for (const resultMessage of toolResultMessages) {
737737 chatCompletion.insertAtStart(resultMessage, 'chatHistory');
@@ -748,7 +748,8 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
748748 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
749749 // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message
750750 if (chatPrompt.role === 'assistant') {
751751 const collectioncontinueMessage = new MessageCollection('continuePrefill', newawait Message.createAsync(chatMessage.role, substituteParams(oai_settings.assistant_prefill + '\n\n') + chatMessage.content, chatMessage.identifier));
752+ const collection = new MessageCollection('continuePrefill', continueMessage);
752753 chatCompletion.add(collection, -1);
753754 continue;
754755 }
@@ -787,18 +788,17 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
787788 * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts.
788789 * @param {Object[]} messageExamples - Array containing all message examples.
789790 */
790791async function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
791792 if (!prompts.has('dialogueExamples')) {
792793 return;
793794 }
794795
795796 chatCompletion.add(new MessageCollection('dialogueExamples'), prompts.index('dialogueExamples'));
796797 if (Array.isArray(messageExamples) && messageExamples.length) {
797798 const newExampleChat = newawait Message.createAsync('system', substituteParams(oai_settings.new_example_chat_prompt), 'newChat');
798799 for (const dialogue of [...messageExamples].forEach((dialogue, dialogueIndex) => {
799800 letconst examplesAddeddialogueIndex = 0messageExamples.indexOf(dialogue);
800-
801+ const chatMessages = [];
801- if (chatCompletion.canAfford(newExampleChat)) chatCompletion.insert(newExampleChat, 'dialogueExamples');
802802
803803 for (let promptIndex = 0; promptIndex < dialogue.length; promptIndex++) {
804804 const prompt = dialogue[promptIndex];
@@ -806,19 +806,20 @@ function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
806806 const content = prompt.content || '';
807807 const identifier = `dialogueExamples ${dialogueIndex}-${promptIndex}`;
808808
809809 const chatMessage = newawait Message.createAsync(role, content, identifier);
810810 await chatMessage.setName(prompt.name);
811- if (!chatCompletion.canAfford(chatMessage)) {
811+ chatMessages.push(chatMessage);
812- break;
813- }
814- chatCompletion.insert(chatMessage, 'dialogueExamples');
815- examplesAdded++;
816812 }
817813
818- if (0 === examplesAdded) {
814+ if (!chatCompletion.canAffordAll([newExampleChat, ...chatMessages])) {
819- chatCompletion.removeLastFrom('dialogueExamples');
815+ break;
820816 }
821- });
817+
818+ chatCompletion.insert(newExampleChat, 'dialogueExamples');
819+ for (const chatMessage of chatMessages) {
820+ chatCompletion.insert(chatMessage, 'dialogueExamples');
821+ }
822+ }
822823 }
823824}
824825
@@ -873,7 +874,7 @@ function getPromptRole(role) {
873874 */
874875async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }) {
875876 // Helper function for preparing a prompt, that already exists within the prompt collection, for completion
876877 const addToChatCompletion = async (source, target = null) => {
877878 // We need the prompts array to determine a position for the source.
878879 if (false === prompts.has(source)) return;
879880
@@ -891,30 +892,31 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
891892
892893 const index = target ? prompts.index(target) : prompts.index(source);
893894 const collection = new MessageCollection(source);
894- collection.add(Message.fromPrompt(prompt));
895+ const message = await Message.fromPromptAsync(prompt);
896+ collection.add(message);
895897 chatCompletion.add(collection, index);
896898 };
897899
898900 chatCompletion.reserveBudget(3); // every reply is primed with <|start|>assistant<|message|>
899901 // Character and world information
900902 await addToChatCompletion('worldInfoBefore');
901903 await addToChatCompletion('main');
902904 await addToChatCompletion('worldInfoAfter');
903905 await addToChatCompletion('charDescription');
904906 await addToChatCompletion('charPersonality');
905907 await addToChatCompletion('scenario');
906908 await addToChatCompletion('personaDescription');
907909
908910 // Collection of control prompts that will always be positioned last
909911 chatCompletion.setOverriddenPrompts(prompts.overriddenPrompts);
910912 const controlPrompts = new MessageCollection('controlPrompts');
911913
912914 const impersonateMessage = await Message.fromPromptfromPromptAsync(prompts.get('impersonate')) ?? null;
913915 if (type === 'impersonate') controlPrompts.add(impersonateMessage);
914916
915917 // Add quiet prompt to control prompts
916918 // This should always be last, even in control prompts. Add all further control prompts BEFORE this prompt
917919 const quietPromptMessage = await Message.fromPromptfromPromptAsync(prompts.get('quietPrompt')) ?? null;
918920 if (quietPromptMessage && quietPromptMessage.content) {
919921 if (isImageInliningSupported() && quietImage) {
920922 await quietPromptMessage.addImage(quietImage);
@@ -940,20 +942,23 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
940942 return acc;
941943 }, []);
942944
943945 for (const identifier of [...systemPrompts, ...userRelativePrompts].forEach(identifier => addToChatCompletion(identifier)); {
946+ await addToChatCompletion(identifier);
947+ }
944948
945949 // Add enhance definition instruction
946950 if (prompts.has('enhanceDefinitions')) await addToChatCompletion('enhanceDefinitions');
947951
948952 // Bias
949953 if (bias && bias.trim().length) await addToChatCompletion('bias');
950954
951955 // Tavern Extras - Summary
952956 if (prompts.has('summary')) {
953957 const summary = prompts.get('summary');
954958
955959 if (summary.position) {
956- chatCompletion.insert(Message.fromPrompt(summary), 'main', summary.position);
960+ const message = await Message.fromPromptAsync(summary);
961+ chatCompletion.insert(message, 'main', summary.position);
957962 }
958963 }
959964
@@ -962,7 +967,8 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
962967 const authorsNote = prompts.get('authorsNote');
963968
964969 if (authorsNote.position) {
965- chatCompletion.insert(Message.fromPrompt(authorsNote), 'main', authorsNote.position);
970+ const message = await Message.fromPromptAsync(authorsNote);
971+ chatCompletion.insert(message, 'main', authorsNote.position);
966972 }
967973 }
968974
@@ -971,7 +977,8 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
971977 const vectorsMemory = prompts.get('vectorsMemory');
972978
973979 if (vectorsMemory.position) {
974- chatCompletion.insert(Message.fromPrompt(vectorsMemory), 'main', vectorsMemory.position);
980+ const message = await Message.fromPromptAsync(vectorsMemory);
981+ chatCompletion.insert(message, 'main', vectorsMemory.position);
975982 }
976983 }
977984
@@ -980,7 +987,8 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
980987 const vectorsDataBank = prompts.get('vectorsDataBank');
981988
982989 if (vectorsDataBank.position) {
983- chatCompletion.insert(Message.fromPrompt(vectorsDataBank), 'main', vectorsDataBank.position);
990+ const message = await Message.fromPromptAsync(vectorsDataBank);
991+ chatCompletion.insert(message, 'main', vectorsDataBank.position);
984992 }
985993 }
986994
@@ -989,13 +997,24 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
989997 const smartContext = prompts.get('smartContext');
990998
991999 if (smartContext.position) {
992- chatCompletion.insert(Message.fromPrompt(smartContext), 'main', smartContext.position);
1000+ const message = await Message.fromPromptAsync(smartContext);
1001+ chatCompletion.insert(message, 'main', smartContext.position);
9931002 }
9941003 }
9951004
9961005 // Other relative extension prompts
9971006 for (const prompt of prompts.collection.filter(p => p.extension && p.position)) {
998- chatCompletion.insert(Message.fromPrompt(prompt), 'main', prompt.position);
1007+ const message = await Message.fromPromptAsync(prompt);
1008+ chatCompletion.insert(message, 'main', prompt.position);
1009+ }
1010+
1011+ // Pre-allocation of tokens for tool data
1012+ if (ToolManager.canPerformToolCalls(type)) {
1013+ const toolData = {};
1014+ await ToolManager.registerFunctionToolsOpenAI(toolData);
1015+ const toolMessage = [{ role: 'user', content: JSON.stringify(toolData) }];
1016+ const toolTokens = await tokenHandler.countAsync(toolMessage);
1017+ chatCompletion.reserveBudget(toolTokens);
9991018 }
10001019
10011020 // Add in-chat injections
@@ -1003,11 +1022,11 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
10031022
10041023 // Decide whether dialogue examples should always be added
10051024 if (power_user.pin_examples) {
10061025 await populateDialogueExamples(prompts, chatCompletion, messageExamples);
10071026 await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt);
10081027 } else {
10091028 await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt);
10101029 await populateDialogueExamples(prompts, chatCompletion, messageExamples);
10111030 }
10121031
10131032 chatCompletion.freeBudget(controlPrompts);
@@ -1272,7 +1291,7 @@ export async function prepareOpenAIMessages({
12721291 promptManager.setChatCompletion(chatCompletion);
12731292
12741293 if (oai_settings.squash_system_messages && dryRun == false) {
12751294 await chatCompletion.squashSystemMessages();
12761295 }
12771296
12781297 // All information is up-to-date, render.
@@ -2118,8 +2137,11 @@ async function calculateLogitBias() {
21182137}
21192138
21202139class TokenHandler {
2121- constructor(countTokenFn) {
2140+ /**
2122- this.countTokenFn = countTokenFn;
2141+ * @param {(messages: object[] | object, full?: boolean) => Promise<number>} countTokenAsyncFn Function to count tokens
2142+ */
2143+ constructor(countTokenAsyncFn) {
2144+ this.countTokenAsyncFn = countTokenAsyncFn;
21232145 this.counts = {
21242146 'start_chat': 0,
21252147 'prompt': 0,
@@ -2148,8 +2170,15 @@ class TokenHandler {
21482170 this.counts[type] -= value;
21492171 }
21502172
2151- count(messages, full, type) {
2173+ /**
2152- const token_count = this.countTokenFn(messages, full);
2174+ * Count tokens for a message or messages.
2175+ * @param {object|any[]} messages Messages to count tokens for
2176+ * @param {boolean} [full] Count full tokens
2177+ * @param {string} [type] Identifier for the token count
2178+ * @returns {Promise<number>} The token count
2179+ */
2180+ async countAsync(messages, full, type) {
2181+ const token_count = await this.countTokenAsyncFn(messages, full);
21532182 this.counts[type] += token_count;
21542183
21552184 return token_count;
@@ -2169,7 +2198,7 @@ class TokenHandler {
21692198}
21702199
21712200
21722201const tokenHandler = new TokenHandler(countTokensOpenAIcountTokensOpenAIAsync);
21732202
21742203// Thrown by ChatCompletion when a requested prompt couldn't be found.
21752204class IdentifierNotFoundError extends Error {
@@ -2219,6 +2248,7 @@ class Message {
22192248 * @param {string} role - The role of the entity creating the message.
22202249 * @param {string} content - The actual content of the message.
22212250 * @param {string} identifier - A unique identifier for the message.
2251+ * @private Don't use this constructor directly. Use createAsync instead.
22222252 */
22232253 constructor(role, content, identifier) {
22242254 this.identifier = identifier;
@@ -2230,18 +2260,32 @@ class Message {
22302260 this.role = 'system';
22312261 }
22322262
2233- if (typeof this.content === 'string' && this.content.length > 0) {
2263+ this.tokens = 0;
2234- this.tokens = tokenHandler.count({ role: this.role, content: this.content });
2264+ }
2235- } else {
2265+
2236- this.tokens = 0;
2266+ /**
2267+ * Create a new Message instance.
2268+ * @param {string} role
2269+ * @param {string} content
2270+ * @param {string} identifier
2271+ * @returns {Promise<Message>} Message instance
2272+ */
2273+ static async createAsync(role, content, identifier) {
2274+ const message = new Message(role, content, identifier);
2275+
2276+ if (typeof message.content === 'string' && message.content.length > 0) {
2277+ message.tokens = await tokenHandler.countAsync({ role: message.role, content: message.content });
22372278 }
2279+
2280+ return message;
22382281 }
22392282
22402283 /**
22412284 * Reconstruct the message from a tool invocation.
22422285 * @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.
2286+ * @returns {Promise<void>}
22432287 */
22442288 async setToolCalls(invocations) {
22452289 this.tool_calls = invocations.map(i => ({
22462290 id: i.id,
22472291 type: 'function',
@@ -2250,14 +2294,24 @@ class Message {
22502294 name: i.name,
22512295 },
22522296 }));
22532297 this.tokens = await tokenHandler.countcountAsync({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
22542298 }
22552299
2256- setName(name) {
2300+ /**
2301+ * Add a name to the message.
2302+ * @param {string} name Name to set for the message.
2303+ * @returns {Promise<void>}
2304+ */
2305+ async setName(name) {
22572306 this.name = name;
22582307 this.tokens = await tokenHandler.countcountAsync({ role: this.role, content: this.content, name: this.name });
22592308 }
22602309
2310+ /**
2311+ * Adds an image to the message.
2312+ * @param {string} image Image URL or Data URL.
2313+ * @returns {Promise<void>}
2314+ */
22612315 async addImage(image) {
22622316 const textContent = this.content;
22632317 const isDataUrl = isDataURL(image);
@@ -2347,13 +2401,13 @@ class Message {
23472401 }
23482402
23492403 /**
23502404 * Create a new Message instance from a prompt asynchronously.
23512405 * @static
23522406 * @param {Object} prompt - The prompt object.
23532407 * @returns {Promise<Message>} A new instance of Message.
23542408 */
23552409 static fromPromptfromPromptAsync(prompt) {
23562410 return new Message.createAsync(prompt.role, prompt.content, prompt.identifier);
23572411 }
23582412
23592413 /**
@@ -2479,8 +2533,9 @@ export class ChatCompletion {
24792533
24802534 /**
24812535 * Combines consecutive system messages into one if they have no name attached.
2536+ * @returns {Promise<void>}
24822537 */
24832538 async squashSystemMessages() {
24842539 const excludeList = ['newMainChat', 'newChat', 'groupNudge'];
24852540 this.messages.collection = this.messages.flatten();
24862541
@@ -2500,7 +2555,7 @@ export class ChatCompletion {
25002555 if (shouldSquash(message)) {
25012556 if (lastMessage && shouldSquash(lastMessage)) {
25022557 lastMessage.content += '\n' + message.content;
25032558 lastMessage.tokens = await tokenHandler.countcountAsync({ role: lastMessage.role, content: lastMessage.content });
25042559 }
25052560 else {
25062561 squashedMessages.push(message);
public/scripts/popup.js+8 -0
@@ -182,6 +182,14 @@ export class Popup {
182182 if (!this.dlg.showModal) {
183183 this.dlg.classList.add('poly_dialog');
184184 dialogPolyfill.registerDialog(this.dlg);
185+ // Force a vertical reposition after the content
186+ // (like crop image) has been set
187+ const resizeObserver = new ResizeObserver((entries) => {
188+ for (const entry of entries) {
189+ dialogPolyfill.reposition(entry.target);
190+ }
191+ });
192+ resizeObserver.observe(this.dlg);
185193 }
186194 this.body = this.dlg.querySelector('.popup-body');
187195 this.content = this.dlg.querySelector('.popup-content');
public/scripts/textgen-models.js+0 -1
@@ -4,7 +4,6 @@ import { textgenerationwebui_settings as textgen_settings, textgen_types } from
44import { tokenizers } from './tokenizers.js';
55import { renderTemplateAsync } from './templates.js';
66import { POPUP_TYPE, callGenericPopup } from './popup.js';
7-import { PAGINATION_TEMPLATE } from './utils.js';
87
98let mancerModels = [];
109let togetherModels = [];
public/scripts/variables.js+82 -3
@@ -1,6 +1,5 @@
11import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
22import { extension_settings, saveMetadataDebounced } from './extensions.js';
3-import { callGenericPopup, POPUP_TYPE } from './popup.js';
43import { executeSlashCommandsWithOptions } from './slash-commands.js';
54import { SlashCommand } from './slash-commands/SlashCommand.js';
65import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
@@ -514,7 +513,8 @@ export function parseBooleanOperands(args) {
514513 return '';
515514 }
516515
517- const operandNumber = Number(operand);
516+ // parseFloat will return NaN for spaces.
517+ const operandNumber = parseFloat(operand);
518518
519519 if (!isNaN(operandNumber)) {
520520 return operandNumber;
@@ -836,6 +836,40 @@ function randValuesCallback(from, to, args) {
836836 return value;
837837}
838838
839+function customSortComparitor(a, b) {
840+ if (typeof a != typeof b) {
841+ a = typeof a;
842+ b = typeof b;
843+ }
844+ return a > b ? 1 : a < b ? -1 : 0;
845+}
846+
847+function sortArrayObjectCallback(args, value) {
848+ let parsedValue;
849+ if (typeof value == 'string') {
850+ try {
851+ parsedValue = JSON.parse(value);
852+ } catch {
853+ // return the original input if it was invalid
854+ return value;
855+ }
856+ } else {
857+ parsedValue = value;
858+ }
859+ if (Array.isArray(parsedValue)) {
860+ // always sort lists by value
861+ parsedValue.sort(customSortComparitor);
862+ } else if (typeof parsedValue == 'object') {
863+ let keysort = args.keysort;
864+ if (isFalseBoolean(keysort)) {
865+ parsedValue = Object.keys(parsedValue).sort(function (a, b) { return customSortComparitor(parsedValue[a], parsedValue[b]); });
866+ } else {
867+ parsedValue = Object.keys(parsedValue).sort(customSortComparitor);
868+ }
869+ }
870+ return JSON.stringify(parsedValue);
871+}
872+
839873/**
840874 * Declare a new variable in the current scope.
841875 * @param {NamedArguments} args Named arguments.
@@ -2110,6 +2144,51 @@ export function registerVariableCommands() {
21102144 `,
21112145 }));
21122146 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2147+ name: 'sort',
2148+ callback: sortArrayObjectCallback,
2149+ returns: 'the sorted list or dictionary keys',
2150+ namedArgumentList: [
2151+ SlashCommandNamedArgument.fromProps({ name: 'keysort',
2152+ description: 'whether to sort by key or value; ignored for lists',
2153+ typeList: [ARGUMENT_TYPE.BOOLEAN],
2154+ enumList: ['true', 'false'],
2155+ defaultValue: 'true',
2156+ }),
2157+ ],
2158+ unnamedArgumentList: [
2159+ SlashCommandArgument.fromProps({
2160+ description: 'value',
2161+ typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
2162+ isRequired: true,
2163+ forceEnum: false,
2164+ }),
2165+ ],
2166+ helpString: `
2167+ <div>
2168+ Sorts a list or dictionary in ascending order and passes the result down the pipe.
2169+ <ul>
2170+ <li>
2171+ For lists, returns the list sorted by value.
2172+ </li>
2173+ <li>
2174+ For dictionaries, returns the ordered list of keys after sorting. Setting keysort=false means keys are sorted by associated value.
2175+ </li>
2176+ </ul>
2177+ </div>
2178+ <div>
2179+ <strong>Examples:</strong>
2180+ <ul>
2181+ <li>
2182+ <pre><code class="language-stscript">/sort [5,3,4,1,2] | /echo</code></pre>
2183+ </li>
2184+ <li>
2185+ <pre><code class="language-stscript">/sort keysort=false {"a": 1, "d": 3, "c": 2, "b": 5} | /echo</code></pre>
2186+ </li>
2187+ </ul>
2188+ </div>
2189+ `,
2190+ }));
2191+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
21132192 name: 'rand',
21142193 callback: (args, value) => String(randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value ? value : 1)), args)),
21152194 returns: 'random number',
public/scripts/world-info.js+34 -27
@@ -130,9 +130,9 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
130130 */
131131class WorldInfoBuffer {
132132 /**
133133 * @type {Map<string, object[]>} ArrayMap of entries that need to be activated no matter what
134134 */
135135 static externalActivations = []new Map();
136136
137137 /**
138138 * @type {string[]} Array of messages sorted by ascending depth
@@ -311,20 +311,19 @@ class WorldInfoBuffer {
311311 }
312312
313313 /**
314314 * Check ifGet the currentexternally activated version of the entry, isif externallythere activatedis one.
315315 * @param {object} entry WI entry to check
316316 * @returns {booleanobject|undefined} Truethe external version if the entry is forcefully activated, undefined otherwise
317317 */
318318 isExternallyActivatedgetExternallyActivated(entry) {
319- // Entries could be copied with structuredClone, so we need to compare them by string representation
319+ return WorldInfoBuffer.externalActivations.get(`${entry.world}.${entry.uid}`);
320- return WorldInfoBuffer.externalActivations.some(x => JSON.stringify(x) === JSON.stringify(entry));
321320 }
322321
323322 /**
324323 * Clean-up the external effects for entries.
325324 */
326325 resetExternalEffects() {
327- WorldInfoBuffer.externalActivations.splice(0, WorldInfoBuffer.externalActivations.length);
326+ WorldInfoBuffer.externalActivations = new Map();
328327 }
329328
330329 /**
@@ -751,7 +750,7 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
751750 worldInfoString = worldInfoBefore + worldInfoAfter;
752751
753752 if (!isDryRun && activatedWorldInfo.allActivatedEntries && activatedWorldInfo.allActivatedEntries.size > 0) {
754753 const arg = Array.from(activatedWorldInfo.allActivatedEntries.values());
755754 await eventSource.emit(event_types.WORLD_INFO_ACTIVATED, arg);
756755 }
757756
@@ -868,7 +867,14 @@ export function setWorldInfoSettings(settings, data) {
868867 });
869868
870869 eventSource.on(event_types.WORLDINFO_FORCE_ACTIVATE, (entries) => {
871- WorldInfoBuffer.externalActivations.push(...entries);
870+ for (const entry of entries) {
871+ if (!Object.hasOwn(entry, 'world') || !Object.hasOwn(entry, 'uid')) {
872+ console.error('[WI] WORLDINFO_FORCE_ACTIVATE requires all entries to have both world and uid fields, entry IGNORED', entry);
873+ } else {
874+ WorldInfoBuffer.externalActivations.set(`${entry.world}.${entry.uid}`, entry);
875+ console.log('[WI] WORLDINFO_FORCE_ACTIVATE added entry', entry);
876+ }
877+ }
872878 });
873879
874880 // Add slash commands
@@ -1680,8 +1686,8 @@ export function sortWorldInfoEntries(data, { customSort = null } = {}) {
16801686 } else if (sortRule === 'priority') {
16811687 // First constant, then normal, then disabled.
16821688 primarySort = (a, b) => {
16831689 const aValue = a.constantdisable ? 02 : a.disableconstant ? 20 : 1;
16841690 const bValue = b.constantdisable ? 02 : b.disableconstant ? 20 : 1;
16851691 return aValue - bValue;
16861692 };
16871693 } else {
@@ -3724,7 +3730,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
37243730 let scanState = scan_state.INITIAL;
37253731 let token_budget_overflowed = false;
37263732 let count = 0;
37273733 let allActivatedEntries = new SetMap();
37283734 let failedProbabilityChecks = new Set();
37293735 let allActivatedText = '';
37303736
@@ -3789,7 +3795,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
37893795 }
37903796
37913797 // Already processed, considered and then skipped entries should still be skipped
37923798 if (failedProbabilityChecks.has(entry) || allActivatedEntries.has(`${entry.world}.${entry.uid}`)) {
37933799 continue;
37943800 }
37953801
@@ -3869,15 +3875,15 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
38693875 continue;
38703876 }
38713877
3872- // Now do checks for immediate activations
3878+ if (buffer.getExternallyActivated(entry)) {
3873- if (entry.constant) {
3879+ log('externally activated');
3874- log('activated because of constant');
3880+ activatedNow.add(buffer.getExternallyActivated(entry));
3875- activatedNow.add(entry);
38763881 continue;
38773882 }
38783883
3879- if (buffer.isExternallyActivated(entry)) {
3884+ // Now do checks for immediate activations
3880- log('externally activated');
3885+ if (entry.constant) {
3886+ log('activated because of constant');
38813887 activatedNow.add(entry);
38823888 continue;
38833889 }
@@ -4039,7 +4045,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
40394045 break;
40404046 }
40414047
4042- allActivatedEntries.add(entry);
4048+ allActivatedEntries.set(`${entry.world}.${entry.uid}`, entry);
40434049 console.debug(`[WI] Entry ${entry.uid} activation successful, adding to prompt`, entry);
40444050 }
40454051
@@ -4123,7 +4129,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
41234129
41244130 // Appends from insertion order 999 to 1. Use unshift for this purpose
41254131 // TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays
41264132 [...allActivatedEntries.values()].sort(sortFn).forEach((entry) => {
41274133 const regexDepth = entry.position === world_info_position.atDepth ? (entry.depth ?? DEFAULT_DEPTH) : null;
41284134 const content = getRegexedString(entry.content, regex_placement.WORLD_INFO, { depth: regexDepth, isMarkdown: false, isPrompt: true });
41294135
@@ -4182,14 +4188,14 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
41824188 context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]);
41834189 }
41844190
41854191 !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries.values()));
41864192 buffer.resetExternalEffects();
41874193 timedEffects.cleanUp();
41884194
41894195 console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
41904196 console.debug('[WI] --- DONE ---');
41914197
41924198 return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
41934199}
41944200
41954201/**
@@ -4291,7 +4297,7 @@ function filterGroupsByTimedEffects(groups, timedEffects, removeEntry) {
42914297/**
42924298 * Filters entries by inclusion groups.
42934299 * @param {object[]} newEntries Entries activated on current recursion level
42944300 * @param {SetMap<string, object>} allActivatedEntries SetMap of all activated entries
42954301 * @param {WorldInfoBuffer} buffer The buffer to use for scanning
42964302 * @param {number} scanState The current scan state
42974303 * @param {WorldInfoTimedEffects} timedEffects The timed effects currently active
@@ -4339,7 +4345,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
43394345 continue;
43404346 }
43414347
43424348 if (Array.from(allActivatedEntries.values()).some(x => x.group === key)) {
43434349 console.debug(`[WI] Skipping inclusion group check, group '${key}' was already activated`);
43444350 // We need to forcefully deactivate all other entries in the group
43454351 removeAllBut(group, null, false);
@@ -4559,6 +4565,7 @@ function convertCharacterBook(characterBook) {
45594565 sticky: entry.extensions?.sticky ?? null,
45604566 cooldown: entry.extensions?.cooldown ?? null,
45614567 delay: entry.extensions?.delay ?? null,
4568+ extensions: entry.extensions ?? {},
45624569 };
45634570 });
45644571
public/style.css+21 -6
@@ -613,6 +613,15 @@ body .panelControlBar {
613613 min-width: 55px;
614614 justify-content: flex-end;
615615 gap: 0px;
616+ transition: all 250ms;
617+}
618+
619+.zoomed_avatar .panelControlBar {
620+opacity:0;
621+}
622+
623+.zoomed_avatar:hover .panelControlBar {
624+ opacity:1;
616625}
617626
618627.panelControlBar .drag-grabber {
@@ -681,9 +690,9 @@ body .panelControlBar {
681690 backdrop-filter: blur(var(--SmartThemeBlurStrength));
682691}
683692
684693/* #send_form.no-connection {
685694 background-color: var(--crimson70a) !important;
686-}
695+} */
687696
688697#nonQRFormItems {
689698 padding: 0;
@@ -2953,10 +2962,11 @@ input[type=search]:focus::-webkit-search-cancel-button {
29532962 bottom: 0;
29542963 position: relative;
29552964 word-break: break-word;
29562965 background-color: var(--black50aSmartThemeBlurTintColor);
29572966 font-size: calc(var(--fontScale) * 0.9em);
29582967 max-height: 50%;
29592968 overflow-y: clip;
2969+ border-radius: 0 0 7px 7px;
29602970}
29612971
29622972.bg_example[custom="true"] .BGSampleTitle {
@@ -3423,7 +3433,7 @@ grammarly-extension {
34233433
34243434.menu_button {
34253435 color: var(--SmartThemeBodyColor);
34263436 background-color: var(--black50aSmartThemeBlurTintColor);
34273437 border: 1px solid var(--SmartThemeBorderColor);
34283438 border-radius: 5px;
34293439 padding: 3px 5px;
@@ -3953,8 +3963,8 @@ input[type="range"]::-webkit-slider-thumb {
39533963 opacity: 0.5;
39543964 margin: 0 5px;
39553965 text-align: center;
39563966 /* border-radius: 100%;
39573967 box-shadow: 0 0 3px black; */
39583968 transition: all 250ms;
39593969}
39603970
@@ -4542,9 +4552,14 @@ h2 {
45424552
45434553a {
45444554 color: orange;
4555+ color: var(--SmartThemeQuoteColor);
45454556 text-decoration: none;
45464557}
45474558
4559+a:hover {
4560+ filter: brightness(1.25);
4561+}
4562+
45484563#export_format_popup {
45494564 display: none;
45504565 z-index: 9999;
recover.js+16 -10
@@ -1,7 +1,13 @@
1-const yaml = require('yaml');
1+import fs from 'node:fs';
2-const fs = require('fs');
2+import process from 'node:process';
3-const storage = require('node-persist');
3+import yaml from 'yaml';
4-const users = require('./src/users');
4+import storage from 'node-persist';
5+import {
6+ initUserStorage,
7+ getPasswordSalt,
8+ getPasswordHash,
9+ toKey,
10+} from './src/users.js';
511
612const userAccount = process.argv[2];
713const userPassword = process.argv[3];
@@ -22,7 +28,7 @@ async function initStorage() {
2228 process.exit(1);
2329 }
2430
2531 await users.initUserStorage(dataRoot);
2632}
2733
2834async function main() {
@@ -31,22 +37,22 @@ async function main() {
3137 /**
3238 * @type {import('./src/users').User}
3339 */
3440 const user = await storage.get(users.toKey(userAccount));
3541
3642 if (!user) {
3743 console.error(`User "${userAccount}" not found.`);
3844 process.exit(1);
3945 }
4046
4147 if (!user.enabled) {
4248 console.log('User is disabled. Enabling...');
4349 user.enabled = true;
4450 }
4551
4652 if (userPassword) {
4753 console.log('Setting new password...');
4854 const salt = users.getPasswordSalt();
4955 const passwordHash = users.getPasswordHash(userPassword, salt);
5056 user.password = passwordHash;
5157 user.salt = salt;
5258 } else {
@@ -55,7 +61,7 @@ async function main() {
5561 user.salt = '';
5662 }
5763
5864 await storage.setItem(users.toKey(userAccount), user);
5965 console.log('User recovered. A program will exit now.');
6066}
6167
server.js+162 -182
@@ -1,32 +1,35 @@
11#!/usr/bin/env node
22
33// native node modules
44constimport fs =from require('node:fs');
55constimport http =from require('node:http');
66constimport https =from require('node:https');
77constimport path =from require('node:path');
88constimport util =from require('node:util');
9+import net from 'node:net';
10+import dns from 'node:dns';
11+import process from 'node:process';
12+import { fileURLToPath } from 'node:url';
913
1014// cli/fs related library imports
1115constimport open =from require('open');
1216constimport yargs =from require('yargs/yargs');
1317constimport { hideBin } =from require('yargs/helpers');
1418
1519// express/server related library imports
1620constimport cors =from require('cors');
1721constimport { doubleCsrf =} require(from 'csrf-csrf').doubleCsrf;
1822constimport express =from require('express');
1923constimport compression =from require('compression');
2024constimport cookieParser =from require('cookie-parser');
2125constimport cookieSession =from require('cookie-session');
2226constimport multer =from require('multer');
2327constimport responseTime =from require('response-time');
2428constimport helmet =from require('helmet').default;
29+import bodyParser from 'body-parser';
2530
2631// net related library imports
27-const net = require('net');
32+import fetch from 'node-fetch';
28-const dns = require('dns');
29-const fetch = require('node-fetch').default;
3033
3134// Unrestrict console logs display limit
3235util.inspect.defaultOptions.maxArrayLength = null;
@@ -34,20 +37,81 @@ util.inspect.defaultOptions.maxStringLength = null;
3437util.inspect.defaultOptions.depth = 4;
3538
3639// local library imports
3740constimport{ userModuleloadPlugins =} require(from './src/usersplugin-loader.js');
38-const basicAuthMiddleware = require('./src/middleware/basicAuth');
41+import {
39-const whitelistMiddleware = require('./src/middleware/whitelist');
42+ initUserStorage,
40-const initRequestProxy = require('./src/request-proxy');
43+ getCsrfSecret,
41-const contentManager = require('./src/endpoints/content-manager');
44+ getCookieSecret,
42-const {
45+ getCookieSessionName,
46+ getAllEnabledUsers,
47+ ensurePublicDirectoriesExist,
48+ getUserDirectoriesList,
49+ migrateSystemPrompts,
50+ migrateUserData,
51+ requireLoginMiddleware,
52+ setUserDataMiddleware,
53+ shouldRedirectToLogin,
54+ tryAutoLogin,
55+ router as userDataRouter,
56+} from './src/users.js';
57+import basicAuthMiddleware from './src/middleware/basicAuth.js';
58+import whitelistMiddleware from './src/middleware/whitelist.js';
59+import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
60+import initRequestProxy from './src/request-proxy.js';
61+import {
4362 getVersion,
4463 getConfigValue,
4564 color,
4665 forwardFetchResponse,
4766 removeColorFormatting,
4867 getSeparator,
4968} =from require('./src/util.js');
5069constimport { ensureThumbnailCacheUPLOADS_DIRECTORY } =from require('./src/endpoints/thumbnailsconstants.js');
70+import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
71+
72+// Routers
73+import { router as usersPublicRouter } from './src/endpoints/users-public.js';
74+import { router as usersPrivateRouter } from './src/endpoints/users-private.js';
75+import { router as usersAdminRouter } from './src/endpoints/users-admin.js';
76+import { router as movingUIRouter } from './src/endpoints/moving-ui.js';
77+import { router as imagesRouter } from './src/endpoints/images.js';
78+import { router as quickRepliesRouter } from './src/endpoints/quick-replies.js';
79+import { router as avatarsRouter } from './src/endpoints/avatars.js';
80+import { router as themesRouter } from './src/endpoints/themes.js';
81+import { router as openAiRouter } from './src/endpoints/openai.js';
82+import { router as googleRouter } from './src/endpoints/google.js';
83+import { router as anthropicRouter } from './src/endpoints/anthropic.js';
84+import { router as tokenizersRouter } from './src/endpoints/tokenizers.js';
85+import { router as presetsRouter } from './src/endpoints/presets.js';
86+import { router as secretsRouter } from './src/endpoints/secrets.js';
87+import { router as thumbnailRouter } from './src/endpoints/thumbnails.js';
88+import { router as novelAiRouter } from './src/endpoints/novelai.js';
89+import { router as extensionsRouter } from './src/endpoints/extensions.js';
90+import { router as assetsRouter } from './src/endpoints/assets.js';
91+import { router as filesRouter } from './src/endpoints/files.js';
92+import { router as charactersRouter } from './src/endpoints/characters.js';
93+import { router as chatsRouter } from './src/endpoints/chats.js';
94+import { router as groupsRouter } from './src/endpoints/groups.js';
95+import { router as worldInfoRouter } from './src/endpoints/worldinfo.js';
96+import { router as statsRouter, init as statsInit, onExit as statsOnExit } from './src/endpoints/stats.js';
97+import { router as backgroundsRouter } from './src/endpoints/backgrounds.js';
98+import { router as spritesRouter } from './src/endpoints/sprites.js';
99+import { router as contentManagerRouter, checkForNewContent } from './src/endpoints/content-manager.js';
100+import { router as settingsRouter, init as settingsInit } from './src/endpoints/settings.js';
101+import { router as stableDiffusionRouter } from './src/endpoints/stable-diffusion.js';
102+import { router as hordeRouter } from './src/endpoints/horde.js';
103+import { router as vectorsRouter } from './src/endpoints/vectors.js';
104+import { router as translateRouter } from './src/endpoints/translate.js';
105+import { router as classifyRouter } from './src/endpoints/classify.js';
106+import { router as captionRouter } from './src/endpoints/caption.js';
107+import { router as searchRouter } from './src/endpoints/search.js';
108+import { router as openRouterRouter } from './src/endpoints/openrouter.js';
109+import { router as chatCompletionsRouter } from './src/endpoints/backends/chat-completions.js';
110+import { router as koboldRouter } from './src/endpoints/backends/kobold.js';
111+import { router as textCompletionsRouter } from './src/endpoints/backends/text-completions.js';
112+import { router as scaleAltRouter } from './src/endpoints/backends/scale-alt.js';
113+import { router as speechRouter } from './src/endpoints/speech.js';
114+import { router as azureRouter } from './src/endpoints/azure.js';
51115
52116// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
53117// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
@@ -166,8 +230,8 @@ const cliArguments = yargs(hideBin(process.argv))
166230 }).parseSync();
167231
168232// change all relative paths
169-console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment.`);
233+const serverDirectory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
170-const serverDirectory = __dirname;
234+console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
171235process.chdir(serverDirectory);
172236
173237const app = express();
@@ -188,7 +252,7 @@ const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMod
188252const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH);
189253const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
190254
191255const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
192256
193257const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
194258const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
@@ -232,7 +296,6 @@ if (listen && basicAuthMode) app.use(basicAuthMiddleware);
232296app.use(whitelistMiddleware(enableWhitelist, listen));
233297
234298if (enableCorsProxy) {
235- const bodyParser = require('body-parser');
236299 app.use(bodyParser.json({
237300 limit: '200mb',
238301 }));
@@ -301,24 +364,23 @@ function getSessionCookieAge() {
301364}
302365
303366app.use(cookieSession({
304367 name: userModule.getCookieSessionName(),
305368 sameSite: 'strict',
306369 httpOnly: true,
307370 maxAge: getSessionCookieAge(),
308371 secret: userModule.getCookieSecret(),
309372}));
310373
311374app.use(userModule.setUserDataMiddleware);
312375
313376// CSRF Protection //
314377if (!disableCsrf) {
315378 const COOKIES_SECRET = userModule.getCookieSecret();
316379
317380 const { generateToken, doubleCsrfProtection } = doubleCsrf({
318381 getSecret: userModule.getCsrfSecret,
319382 cookieName: 'X-CSRF-Token',
320383 cookieOptions: {
321- httpOnly: true,
322384 sameSite: 'strict',
323385 secure: false,
324386 },
@@ -346,7 +408,7 @@ if (!disableCsrf) {
346408// Static files
347409// Host index page
348410app.get('/', (request, response) => {
349411 if (userModule.shouldRedirectToLogin(request)) {
350412 const query = request.url.split('?')[1];
351413 const redirectUrl = query ? `/login?${query}` : '/login';
352414 return response.redirect(redirectUrl);
@@ -363,7 +425,7 @@ app.get('/login', async (request, response) => {
363425 }
364426
365427 try {
366428 const autoLogin = await userModule.tryAutoLogin(request, basicAuthMode);
367429
368430 if (autoLogin) {
369431 return response.redirect('/');
@@ -379,22 +441,22 @@ app.get('/login', async (request, response) => {
379441app.use(express.static(process.cwd() + '/public', {}));
380442
381443// Public API
382444app.use('/api/users', require('./src/endpoints/users-public').routerusersPublicRouter);
383445
384446// Everything below this line requires authentication
385447app.use(userModule.requireLoginMiddleware);
386448app.get('/api/ping', (_, response) => response.sendStatus(204));
387449
388450// File uploads
389451app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
390452app.use(require('./src/middleware/multerMonkeyPatch'));
391453
392454// User data mount
393455app.use('/', userModule.routeruserDataRouter);
394456// Private endpoints
395457app.use('/api/users', require('./src/endpoints/users-private').routerusersPrivateRouter);
396458// Admin endpoints
397459app.use('/api/users', require('./src/endpoints/users-admin').routerusersAdminRouter);
398460
399461app.get('/version', async function (_, response) {
400462 const data = await getVersion();
@@ -509,126 +571,45 @@ redirect('/api/serpapi/search', '/api/search/serpapi');
509571redirect('/api/serpapi/visit', '/api/search/visit');
510572redirect('/api/serpapi/transcript', '/api/search/transcript');
511573
512-// Moving UI
574+app.use('/api/moving-ui', movingUIRouter);
513575app.use('/api/moving-uiimages', require('./src/endpoints/moving-ui').routerimagesRouter);
514-
576+app.use('/api/quick-replies', quickRepliesRouter);
515-// Image management
577+app.use('/api/avatars', avatarsRouter);
516578app.use('/api/imagesthemes', require('./src/endpoints/images').routerthemesRouter);
517-
579+app.use('/api/openai', openAiRouter);
518-// Quick reply management
580+app.use('/api/google', googleRouter);
519581app.use('/api/quick-repliesanthropic', require('./src/endpoints/quick-replies').routeranthropicRouter);
520-
582+app.use('/api/tokenizers', tokenizersRouter);
521-// Avatar management
583+app.use('/api/presets', presetsRouter);
522584app.use('/api/avatarssecrets', require('./src/endpoints/avatars').routersecretsRouter);
523-
585+app.use('/thumbnail', thumbnailRouter);
524-// Theme management
586+app.use('/api/novelai', novelAiRouter);
525587app.use('/api/themesextensions', require('./src/endpoints/themes').routerextensionsRouter);
526-
588+app.use('/api/assets', assetsRouter);
527-// OpenAI API
589+app.use('/api/files', filesRouter);
528590app.use('/api/openaicharacters', require('./src/endpoints/openai').routercharactersRouter);
529-
591+app.use('/api/chats', chatsRouter);
530-//Google API
592+app.use('/api/groups', groupsRouter);
531593app.use('/api/googleworldinfo', require('./src/endpoints/google').routerworldInfoRouter);
532-
594+app.use('/api/stats', statsRouter);
533-//Anthropic API
595+app.use('/api/backgrounds', backgroundsRouter);
534596app.use('/api/anthropicsprites', require('./src/endpoints/anthropic').routerspritesRouter);
535-
597+app.use('/api/content', contentManagerRouter);
536-// Tokenizers
598+app.use('/api/settings', settingsRouter);
537599app.use('/api/tokenizerssd', require('./src/endpoints/tokenizers').routerstableDiffusionRouter);
538-
600+app.use('/api/horde', hordeRouter);
539-// Preset management
601+app.use('/api/vector', vectorsRouter);
540602app.use('/api/presetstranslate', require('./src/endpoints/presets').routertranslateRouter);
541-
603+app.use('/api/extra/classify', classifyRouter);
542-// Secrets managemenet
604+app.use('/api/extra/caption', captionRouter);
543605app.use('/api/secretssearch', require('./src/endpoints/secrets').routersearchRouter);
544-
606+app.use('/api/backends/text-completions', textCompletionsRouter);
545-// Thumbnail generation. These URLs are saved in chat, so this route cannot be renamed!
607+app.use('/api/openrouter', openRouterRouter);
546608app.use('/thumbnail', require('./srcapi/endpointsbackends/thumbnailskobold').router, koboldRouter);
547-
609+app.use('/api/backends/chat-completions', chatCompletionsRouter);
548-// NovelAI generation
610+app.use('/api/backends/scale-alt', scaleAltRouter);
549611app.use('/api/novelaispeech', require('./src/endpoints/novelai').routerspeechRouter);
550-
612+app.use('/api/azure', azureRouter);
551-// Third-party extensions
552-app.use('/api/extensions', require('./src/endpoints/extensions').router);
553-
554-// Asset management
555-app.use('/api/assets', require('./src/endpoints/assets').router);
556-
557-// File management
558-app.use('/api/files', require('./src/endpoints/files').router);
559-
560-// Character management
561-app.use('/api/characters', require('./src/endpoints/characters').router);
562-
563-// Chat management
564-app.use('/api/chats', require('./src/endpoints/chats').router);
565-
566-// Group management
567-app.use('/api/groups', require('./src/endpoints/groups').router);
568-
569-// World info management
570-app.use('/api/worldinfo', require('./src/endpoints/worldinfo').router);
571-
572-// Stats calculation
573-const statsEndpoint = require('./src/endpoints/stats');
574-app.use('/api/stats', statsEndpoint.router);
575-
576-// Background management
577-app.use('/api/backgrounds', require('./src/endpoints/backgrounds').router);
578-
579-// Character sprite management
580-app.use('/api/sprites', require('./src/endpoints/sprites').router);
581-
582-// Custom content management
583-app.use('/api/content', require('./src/endpoints/content-manager').router);
584-
585-// Settings load/store
586-const settingsEndpoint = require('./src/endpoints/settings');
587-app.use('/api/settings', settingsEndpoint.router);
588-
589-// Stable Diffusion generation
590-app.use('/api/sd', require('./src/endpoints/stable-diffusion').router);
591-
592-// LLM and SD Horde generation
593-app.use('/api/horde', require('./src/endpoints/horde').router);
594-
595-// Vector storage DB
596-app.use('/api/vector', require('./src/endpoints/vectors').router);
597-
598-// Chat translation
599-app.use('/api/translate', require('./src/endpoints/translate').router);
600-
601-// Emotion classification
602-app.use('/api/extra/classify', require('./src/endpoints/classify').router);
603-
604-// Image captioning
605-app.use('/api/extra/caption', require('./src/endpoints/caption').router);
606-
607-// Web search and scraping
608-app.use('/api/search', require('./src/endpoints/search').router);
609-
610-// The different text generation APIs
611-
612-// Ooba/OpenAI text completions
613-app.use('/api/backends/text-completions', require('./src/endpoints/backends/text-completions').router);
614-
615-// OpenRouter
616-app.use('/api/openrouter', require('./src/endpoints/openrouter').router);
617-
618-// KoboldAI
619-app.use('/api/backends/kobold', require('./src/endpoints/backends/kobold').router);
620-
621-// OpenAI chat completions
622-app.use('/api/backends/chat-completions', require('./src/endpoints/backends/chat-completions').router);
623-
624-// Scale (alt method)
625-app.use('/api/backends/scale-alt', require('./src/endpoints/backends/scale-alt').router);
626-
627-// Speech (text-to-speech and speech-to-text)
628-app.use('/api/speech', require('./src/endpoints/speech').router);
629-
630-// Azure TTS
631-app.use('/api/azure', require('./src/endpoints/azure').router);
632613
633614const tavernUrlV6 = new URL(
634615 (cliArguments.ssl ? 'https://' : 'http://') +
@@ -658,22 +639,22 @@ const preSetupTasks = async function () {
658639 }
659640 console.log();
660641
661642 const directories = await userModule.getUserDirectoriesList();
662643 await contentManager.checkForNewContent(directories);
663644 await ensureThumbnailCache();
664645 cleanUploads();
665646
666647 await settingsEndpoint.initsettingsInit();
667648 await statsEndpoint.initstatsInit();
668649
669650 const cleanupPlugins = await loadPluginsinitializePlugins();
670651 const consoleTitle = process.title;
671652
672653 let isExiting = false;
673654 const exitProcess = async () => {
674655 if (isExiting) return;
675656 isExiting = true;
676657 statsEndpoint.onExitawait statsOnExit();
677658 if (typeof cleanupPlugins === 'function') {
678659 await cleanupPlugins();
679660 }
@@ -773,11 +754,10 @@ const postSetupTasks = async function (v6Failed, v4Failed) {
773754 * Loads server plugins from a directory.
774755 * @returns {Promise<Function>} Function to be run on server exit
775756 */
776757async function loadPluginsinitializePlugins() {
777758 try {
778759 const pluginDirectory = path.join(serverDirectory, 'plugins');
779- const loader = require('./src/plugin-loader');
760+ const cleanupPlugins = await loadPlugins(app, pluginDirectory);
780- const cleanupPlugins = await loader.loadPlugins(app, pluginDirectory);
781761 return cleanupPlugins;
782762 } catch {
783763 console.log('Plugin loading failed.');
@@ -850,7 +830,7 @@ function createHttpsServer(url) {
850830 }, app);
851831 server.on('error', reject);
852832 server.on('listening', resolve);
853833 server.listen(Number(url.port || 443), url.hostname);
854834 });
855835}
856836
@@ -865,7 +845,7 @@ function createHttpServer(url) {
865845 const server = http.createServer(app);
866846 server.on('error', reject);
867847 server.on('listening', resolve);
868848 server.listen(Number(url.port || 80), url.hostname);
869849 });
870850}
871851
@@ -917,7 +897,7 @@ async function verifySecuritySettings() {
917897 logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');
918898 }
919899
920900 const users = await userModule.getAllEnabledUsers();
921901 const unprotectedUsers = users.filter(x => !x.password);
922902 const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin);
923903
@@ -935,10 +915,10 @@ async function verifySecuritySettings() {
935915}
936916
937917// User storage module needs to be initialized before starting the server
938918userModule.initUserStorage(dataRoot)
939919 .then(userModule.ensurePublicDirectoriesExist)
940920 .then(userModule.migrateUserData)
941921 .then(userModule.migrateSystemPrompts)
942922 .then(verifySecuritySettings)
943923 .then(preSetupTasks)
944924 .finally(startServer);
src/additional-headers.js+20 -26
@@ -1,10 +1,10 @@
11constimport { TEXTGEN_TYPES, OPENROUTER_HEADERS, FEATHERLESS_HEADERS } =from require('./constants.js');
22constimport { SECRET_KEYS, readSecret } =from require('./endpoints/secrets.js');
33constimport { getConfigValue } =from require('./util.js');
44
55/**
66 * Gets the headers for the Mancer API.
77 * @param {import('./users.js').UserDirectoryList} directories User directories
88 * @returns {object} Headers for the request
99 */
1010function getMancerHeaders(directories) {
@@ -18,7 +18,7 @@ function getMancerHeaders(directories) {
1818
1919/**
2020 * Gets the headers for the TogetherAI API.
2121 * @param {import('./users.js').UserDirectoryList} directories User directories
2222 * @returns {object} Headers for the request
2323 */
2424function getTogetherAIHeaders(directories) {
@@ -31,7 +31,7 @@ function getTogetherAIHeaders(directories) {
3131
3232/**
3333 * Gets the headers for the InfermaticAI API.
3434 * @param {import('./users.js').UserDirectoryList} directories User directories
3535 * @returns {object} Headers for the request
3636 */
3737function getInfermaticAIHeaders(directories) {
@@ -44,7 +44,7 @@ function getInfermaticAIHeaders(directories) {
4444
4545/**
4646 * Gets the headers for the DreamGen API.
4747 * @param {import('./users.js').UserDirectoryList} directories User directories
4848 * @returns {object} Headers for the request
4949 */
5050function getDreamGenHeaders(directories) {
@@ -57,7 +57,7 @@ function getDreamGenHeaders(directories) {
5757
5858/**
5959 * Gets the headers for the OpenRouter API.
6060 * @param {import('./users.js').UserDirectoryList} directories User directories
6161 * @returns {object} Headers for the request
6262 */
6363function getOpenRouterHeaders(directories) {
@@ -69,7 +69,7 @@ function getOpenRouterHeaders(directories) {
6969
7070/**
7171 * Gets the headers for the vLLM API.
7272 * @param {import('./users.js').UserDirectoryList} directories User directories
7373 * @returns {object} Headers for the request
7474 */
7575function getVllmHeaders(directories) {
@@ -82,7 +82,7 @@ function getVllmHeaders(directories) {
8282
8383/**
8484 * Gets the headers for the Aphrodite API.
8585 * @param {import('./users.js').UserDirectoryList} directories User directories
8686 * @returns {object} Headers for the request
8787 */
8888function getAphroditeHeaders(directories) {
@@ -96,7 +96,7 @@ function getAphroditeHeaders(directories) {
9696
9797/**
9898 * Gets the headers for the Tabby API.
9999 * @param {import('./users.js').UserDirectoryList} directories User directories
100100 * @returns {object} Headers for the request
101101 */
102102function getTabbyHeaders(directories) {
@@ -110,7 +110,7 @@ function getTabbyHeaders(directories) {
110110
111111/**
112112 * Gets the headers for the LlamaCPP API.
113113 * @param {import('./users.js').UserDirectoryList} directories User directories
114114 * @returns {object} Headers for the request
115115 */
116116function getLlamaCppHeaders(directories) {
@@ -123,7 +123,7 @@ function getLlamaCppHeaders(directories) {
123123
124124/**
125125 * Gets the headers for the Ooba API.
126126 * @param {import('./users.js').UserDirectoryList} directories
127127 * @returns {object} Headers for the request
128128 */
129129function getOobaHeaders(directories) {
@@ -136,7 +136,7 @@ function getOobaHeaders(directories) {
136136
137137/**
138138 * Gets the headers for the KoboldCpp API.
139139 * @param {import('./users.js').UserDirectoryList} directories
140140 * @returns {object} Headers for the request
141141 */
142142function getKoboldCppHeaders(directories) {
@@ -149,7 +149,7 @@ function getKoboldCppHeaders(directories) {
149149
150150/**
151151 * Gets the headers for the Featherless API.
152152 * @param {import('./users.js').UserDirectoryList} directories
153153 * @returns {object} Headers for the request
154154 */
155155function getFeatherlessHeaders(directories) {
@@ -161,7 +161,7 @@ function getFeatherlessHeaders(directories) {
161161
162162/**
163163 * Gets the headers for the HuggingFace API.
164164 * @param {import('./users.js').UserDirectoryList} directories
165165 * @returns {object} Headers for the request
166166 */
167167function getHuggingFaceHeaders(directories) {
@@ -172,7 +172,7 @@ function getHuggingFaceHeaders(directories) {
172172 }) : {};
173173}
174174
175175export function getOverrideHeaders(urlHost) {
176176 const requestOverrides = getConfigValue('requestOverrides', []);
177177 const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers;
178178 if (overrideHeaders && urlHost) {
@@ -188,7 +188,7 @@ function getOverrideHeaders(urlHost) {
188188 * @param {object} args New request arguments
189189 * @param {string|null} server API server for new request
190190 */
191191export function setAdditionalHeaders(request, args, server) {
192192 setAdditionalHeadersByType(args.headers, request.body.api_type, server, request.user.directories);
193193}
194194
@@ -197,9 +197,9 @@ function setAdditionalHeaders(request, args, server) {
197197 * @param {object} requestHeaders Request headers
198198 * @param {string} type API type
199199 * @param {string|null} server API server for new request
200200 * @param {import('./users.js').UserDirectoryList} directories User directories
201201 */
202202export function setAdditionalHeadersByType(requestHeaders, type, server, directories) {
203203 const headerGetters = {
204204 [TEXTGEN_TYPES.MANCER]: getMancerHeaders,
205205 [TEXTGEN_TYPES.VLLM]: getVllmHeaders,
@@ -234,9 +234,3 @@ function setAdditionalHeadersByType(requestHeaders, type, server, directories) {
234234
235235 Object.assign(requestHeaders, headers);
236236}
237-
238-module.exports = {
239- getOverrideHeaders,
240- setAdditionalHeaders,
241- setAdditionalHeadersByType,
242-};
src/character-card-parser.js+13 -15
@@ -1,8 +1,9 @@
11constimport fs =from require('node:fs');
2+import { Buffer } from 'node:buffer';
23
34constimport encode =from require('png-chunks-encode');
45constimport extract =from require('png-chunks-extract');
56constimport PNGtext =from require('png-chunk-text');
67
78/**
89 * Writes Character metadata to a PNG image buffer.
@@ -11,8 +12,8 @@ const PNGtext = require('png-chunk-text');
1112 * @param {string} data Character data to write
1213 * @returns {Buffer} PNG image buffer with metadata
1314 */
1415export const write = (image, data) => {
1516 const chunks = extract(new Uint8Array(image));
1617 const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
1718
1819 // Remove existing tEXt chunks
@@ -36,7 +37,9 @@ const write = (image, data) => {
3637
3738 const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64');
3839 chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData));
3940 } catch (error) { }
41+ // Ignore errors when adding v3 chunk
42+ }
4043
4144 const newBuffer = Buffer.from(encode(chunks));
4245 return newBuffer;
@@ -48,8 +51,8 @@ const write = (image, data) => {
4851 * @param {Buffer} image PNG image buffer
4952 * @returns {string} Character data
5053 */
5154export const read = (image) => {
5255 const chunks = extract(new Uint8Array(image));
5356
5457 const textChunks = chunks.filter((chunk) => chunk.name === 'tEXt').map((chunk) => PNGtext.decode(chunk.data));
5558
@@ -80,7 +83,7 @@ const read = (image) => {
8083 * @param {string} format File format
8184 * @returns {string} Character data
8285 */
8386export const parse = (cardUrl, format) => {
8487 let fileFormat = format === undefined ? 'png' : format;
8588
8689 switch (fileFormat) {
@@ -93,8 +96,3 @@ const parse = (cardUrl, format) => {
9396 throw new Error('Unsupported format');
9497};
9598
96-module.exports = {
97- parse,
98- write,
99- read,
100-};
src/constants.js+23 -48
@@ -1,18 +1,18 @@
11export const PUBLIC_DIRECTORIES = {
22 images: 'public/img/',
33 backups: 'backups/',
44 sounds: 'public/sounds',
55 extensions: 'public/scripts/extensions',
66};
77
88export const SETTINGS_FILE = 'settings.json';
99
1010/**
1111 * @type {import('./users.js').UserDirectoryList}
1212 * @readonly
1313 * @enum {string}
1414 */
1515export const USER_DIRECTORY_TEMPLATE = Object.freeze({
1616 root: '',
1717 thumbnails: 'thumbnails',
1818 thumbnailsBg: 'thumbnails/bg',
@@ -45,10 +45,10 @@ const USER_DIRECTORY_TEMPLATE = Object.freeze({
4545});
4646
4747/**
4848 * @type {import('./users.js').User}
4949 * @readonly
5050 */
5151export const DEFAULT_USER = Object.freeze({
5252 handle: 'default-user',
5353 name: 'User',
5454 created: Date.now(),
@@ -58,7 +58,7 @@ const DEFAULT_USER = Object.freeze({
5858 salt: '',
5959});
6060
6161export const UNSAFE_EXTENSIONS = [
6262 '.php',
6363 '.exe',
6464 '.com',
@@ -135,7 +135,7 @@ const UNSAFE_EXTENSIONS = [
135135 '.ws',
136136];
137137
138138export const GEMINI_SAFETY = [
139139 {
140140 category: 'HARM_CATEGORY_HARASSMENT',
141141 threshold: 'BLOCK_NONE',
@@ -158,7 +158,7 @@ const GEMINI_SAFETY = [
158158 },
159159];
160160
161161export const BISON_SAFETY = [
162162 {
163163 category: 'HARM_CATEGORY_DEROGATORY',
164164 threshold: 'BLOCK_NONE',
@@ -185,7 +185,7 @@ const BISON_SAFETY = [
185185 },
186186];
187187
188188export const CHAT_COMPLETION_SOURCES = {
189189 OPENAI: 'openai',
190190 WINDOWAI: 'windowai',
191191 CLAUDE: 'claude',
@@ -205,10 +205,10 @@ const CHAT_COMPLETION_SOURCES = {
205205/**
206206 * Path to multer file uploads under the data root.
207207 */
208208export const UPLOADS_DIRECTORY = '_uploads';
209209
210210// TODO: this is copied from the client code; there should be a way to de-duplicate it eventually
211211export const TEXTGEN_TYPES = {
212212 OOBA: 'ooba',
213213 MANCER: 'mancer',
214214 VLLM: 'vllm',
@@ -225,7 +225,7 @@ const TEXTGEN_TYPES = {
225225 HUGGINGFACE: 'huggingface',
226226};
227227
228228export const INFERMATICAI_KEYS = [
229229 'model',
230230 'prompt',
231231 'max_tokens',
@@ -248,7 +248,7 @@ const INFERMATICAI_KEYS = [
248248 'logprobs',
249249];
250250
251251export const FEATHERLESS_KEYS = [
252252 'model',
253253 'prompt',
254254 'best_of',
@@ -290,9 +290,8 @@ const FEATHERLESS_KEYS = [
290290 'guided_whitespace_pattern',
291291];
292292
293-
294293// https://dreamgen.com/docs/api#openai-text
295294export const DREAMGEN_KEYS = [
296295 'model',
297296 'prompt',
298297 'max_tokens',
@@ -309,7 +308,7 @@ const DREAMGEN_KEYS = [
309308];
310309
311310// https://docs.together.ai/reference/completions
312311export const TOGETHERAI_KEYS = [
313312 'model',
314313 'prompt',
315314 'max_tokens',
@@ -325,7 +324,7 @@ const TOGETHERAI_KEYS = [
325324];
326325
327326// https://github.com/jmorganca/ollama/blob/main/docs/api.md#request-with-options
328327export const OLLAMA_KEYS = [
329328 'num_predict',
330329 'num_ctx',
331330 'stop',
@@ -345,20 +344,20 @@ const OLLAMA_KEYS = [
345344 'min_p',
346345];
347346
348347export const AVATAR_WIDTH = 512;
349348export const AVATAR_HEIGHT = 768;
350349
351350export const OPENROUTER_HEADERS = {
352351 'HTTP-Referer': 'https://sillytavern.app',
353352 'X-Title': 'SillyTavern',
354353};
355354
356355export const FEATHERLESS_HEADERS = {
357356 'HTTP-Referer': 'https://sillytavern.app',
358357 'X-Title': 'SillyTavern',
359358};
360359
361360export const OPENROUTER_KEYS = [
362361 'max_tokens',
363362 'temperature',
364363 'top_k',
@@ -378,7 +377,7 @@ const OPENROUTER_KEYS = [
378377];
379378
380379// https://github.com/vllm-project/vllm/blob/0f8a91401c89ac0a8018def3756829611b57727f/vllm/entrypoints/openai/protocol.py#L220
381380export const VLLM_KEYS = [
382381 'model',
383382 'prompt',
384383 'best_of',
@@ -419,27 +418,3 @@ const VLLM_KEYS = [
419418 'guided_decoding_backend',
420419 'guided_whitespace_pattern',
421420];
422-
423-module.exports = {
424- DEFAULT_USER,
425- SETTINGS_FILE,
426- PUBLIC_DIRECTORIES,
427- USER_DIRECTORY_TEMPLATE,
428- UNSAFE_EXTENSIONS,
429- UPLOADS_DIRECTORY,
430- GEMINI_SAFETY,
431- BISON_SAFETY,
432- TEXTGEN_TYPES,
433- CHAT_COMPLETION_SOURCES,
434- AVATAR_WIDTH,
435- AVATAR_HEIGHT,
436- TOGETHERAI_KEYS,
437- OLLAMA_KEYS,
438- INFERMATICAI_KEYS,
439- DREAMGEN_KEYS,
440- OPENROUTER_HEADERS,
441- OPENROUTER_KEYS,
442- VLLM_KEYS,
443- FEATHERLESS_KEYS,
444- FEATHERLESS_HEADERS,
445-};
src/endpoints/anthropic.js+7 -8
@@ -1,9 +1,10 @@
1-const { readSecret, SECRET_KEYS } = require('./secrets');
1+import fetch from 'node-fetch';
2-const fetch = require('node-fetch').default;
2+import express from 'express';
3-const express = require('express');
4-const { jsonParser } = require('../express-common');
53
6-const router = express.Router();
4+import { readSecret, SECRET_KEYS } from './secrets.js';
5+import { jsonParser } from '../express-common.js';
6+
7+export const router = express.Router();
78
89router.post('/caption-image', jsonParser, async (request, response) => {
910 try {
@@ -41,7 +42,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4142 'anthropic-version': '2023-06-01',
4243 'x-api-key': request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE),
4344 },
44- timeout: 0,
4545 });
4646
4747 if (!result.ok) {
@@ -50,6 +50,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
5050 return response.status(result.status).send({ error: true });
5151 }
5252
53+ /** @type {any} */
5354 const generateResponseJson = await result.json();
5455 const caption = generateResponseJson.content[0].text;
5556 console.log('Claude response:', generateResponseJson);
@@ -64,5 +65,3 @@ router.post('/caption-image', jsonParser, async (request, response) => {
6465 response.status(500).send('Internal server error');
6566 }
6667});
67-
68-module.exports = { router };
src/endpoints/assets.js+15 -15
@@ -1,13 +1,15 @@
11constimport path =from require('node:path');
22constimport fs =from require('node:fs');
3-const mime = require('mime-types');
3+import { finished } from 'node:stream/promises';
4-const express = require('express');
4+
55constimport sanitizemime =from require('sanitizemime-filenametypes');
6-const fetch = require('node-fetch').default;
6+import express from 'express';
7-const { finished } = require('stream/promises');
7+import sanitize from 'sanitize-filename';
8-const { UNSAFE_EXTENSIONS } = require('../constants');
8+import fetch from 'node-fetch';
9-const { jsonParser } = require('../express-common');
9+
1010constimport { clientRelativePathUNSAFE_EXTENSIONS } =from require('../utilconstants.js');
11+import { jsonParser } from '../express-common.js';
12+import { clientRelativePath } from '../util.js';
1113
1214const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm', 'character', 'temp'];
1315
@@ -16,7 +18,7 @@ const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm', 'character'
1618 * @param {string} inputFilename Input filename
1719 * @returns {{error: boolean, message?: string}} Whether validation failed, and why if so
1820 */
1921export function validateAssetFileName(inputFilename) {
2022 if (!/^[a-zA-Z0-9_\-.]+$/.test(inputFilename)) {
2123 return {
2224 error: true,
@@ -77,7 +79,7 @@ function getFiles(dir, files = []) {
7779
7880/**
7981 * Ensure that the asset folders exist.
8082 * @param {import('../users.js').UserDirectoryList} directories - The user's directories
8183 */
8284function ensureFoldersExist(directories) {
8385 const folderPath = path.join(directories.assets);
@@ -93,7 +95,7 @@ function ensureFoldersExist(directories) {
9395 }
9496}
9597
9698export const router = express.Router();
9799
98100/**
99101 * HTTP POST handler function to retrieve name of all files of a given folder path.
@@ -366,5 +368,3 @@ router.post('/character', jsonParser, async (request, response) => {
366368 return response.sendStatus(500);
367369 }
368370});
369-
370-module.exports = { router, validateAssetFileName };
src/endpoints/avatars.js+11 -13
@@ -1,16 +1,16 @@
1-const express = require('express');
1+import path from 'node:path';
2-const path = require('path');
2+import fs from 'node:fs';
3-const fs = require('fs');
4-const sanitize = require('sanitize-filename');
5-const writeFileAtomicSync = require('write-file-atomic').sync;
6-const { jsonParser, urlencodedParser } = require('../express-common');
7-const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
8-const { getImages, tryParse } = require('../util');
93
10-// image processing related library imports
4+import express from 'express';
11-const jimp = require('jimp');
5+import sanitize from 'sanitize-filename';
6+import jimp from 'jimp';
7+import { sync as writeFileAtomicSync } from 'write-file-atomic';
128
13-const router = express.Router();
9+import { jsonParser, urlencodedParser } from '../express-common.js';
10+import { AVATAR_WIDTH, AVATAR_HEIGHT } from '../constants.js';
11+import { getImages, tryParse } from '../util.js';
12+
13+export const router = express.Router();
1414
1515router.post('/get', jsonParser, function (request, response) {
1616 var images = getImages(request.user.directories.avatars);
@@ -58,5 +58,3 @@ router.post('/upload', urlencodedParser, async (request, response) => {
5858 return response.status(400).send('Is not a valid image');
5959 }
6060});
61-
62-module.exports = { router };
src/endpoints/azure.js+6 -9
@@ -1,9 +1,10 @@
1-const { readSecret, SECRET_KEYS } = require('./secrets');
1+import fetch from 'node-fetch';
2-const fetch = require('node-fetch').default;
2+import { Router } from 'express';
3-const express = require('express');
4-const { jsonParser } = require('../express-common');
53
6-const router = express.Router();
4+import { readSecret, SECRET_KEYS } from './secrets.js';
5+import { jsonParser } from '../express-common.js';
6+
7+export const router = Router();
78
89router.post('/list', jsonParser, async (req, res) => {
910 try {
@@ -86,7 +87,3 @@ router.post('/generate', jsonParser, async (req, res) => {
8687 return res.sendStatus(500);
8788 }
8889});
89-
90-module.exports = {
91- router,
92-};
src/endpoints/backends/chat-completions.js+45 -56
@@ -1,13 +1,41 @@
1-const express = require('express');
1+import process from 'node:process';
2-const fetch = require('node-fetch').default;
2+import express from 'express';
3-
3+import fetch from 'node-fetch';
4-const { jsonParser } = require('../../express-common');
4+
55constimport { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERSjsonParser } =from require('../../constantsexpress-common.js');
6-const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
6+import {
7-const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertAI21Messages, mergeMessages } = require('../../prompt-converters');
7+ CHAT_COMPLETION_SOURCES,
8-
8+ GEMINI_SAFETY,
9-const { readSecret, SECRET_KEYS } = require('../secrets');
9+ BISON_SAFETY,
10-const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
10+ OPENROUTER_HEADERS,
11+} from '../../constants.js';
12+import {
13+ forwardFetchResponse,
14+ getConfigValue,
15+ tryParse,
16+ uuidv4,
17+ mergeObjectWithYaml,
18+ excludeKeysByYaml,
19+ color,
20+} from '../../util.js';
21+import {
22+ convertClaudeMessages,
23+ convertGooglePrompt,
24+ convertTextCompletionPrompt,
25+ convertCohereMessages,
26+ convertMistralMessages,
27+ convertAI21Messages,
28+ mergeMessages,
29+} from '../../prompt-converters.js';
30+
31+import { readSecret, SECRET_KEYS } from '../secrets.js';
32+import {
33+ getTokenizerModel,
34+ getSentencepiceTokenizer,
35+ getTiktokenTokenizer,
36+ sentencepieceTokenizers,
37+ TEXT_COMPLETION_MODELS,
38+} from '../tokenizers.js';
1139
1240const API_OPENAI = 'https://api.openai.com/v1';
1341const API_CLAUDE = 'https://api.anthropic.com/v1';
@@ -42,43 +70,6 @@ function postProcessPrompt(messages, type, charName, userName) {
4270}
4371
4472/**
45- * Ollama strikes back. Special boy #2's steaming routine.
46- * Wrap this abomination into proper SSE stream, again.
47- * @param {Response} jsonStream JSON stream
48- * @param {import('express').Request} request Express request
49- * @param {import('express').Response} response Express response
50- * @returns {Promise<any>} Nothing valuable
51- */
52-async function parseCohereStream(jsonStream, request, response) {
53- try {
54- const stream = new CohereStream({ stream: jsonStream.body, eventShape: { type: 'json', messageTerminator: '\n' } });
55-
56- for await (const json of stream.iterMessages()) {
57- if (json.message) {
58- const message = json.message || 'Unknown error';
59- const chunk = { error: { message: message } };
60- response.write(`data: ${JSON.stringify(chunk)}\n\n`);
61- } else if (json.event_type === 'text-generation') {
62- const text = json.text || '';
63- const chunk = { choices: [{ text }] };
64- response.write(`data: ${JSON.stringify(chunk)}\n\n`);
65- }
66- }
67-
68- console.log('Streaming request finished');
69- response.write('data: [DONE]\n\n');
70- response.end();
71- } catch (error) {
72- console.log('Error forwarding streaming response:', error);
73- if (!response.headersSent) {
74- return response.status(500).send({ error: true });
75- } else {
76- return response.end();
77- }
78- }
79-}
80-
81-/**
8273 * Sends a request to Claude API.
8374 * @param {express.Request} request Express request
8475 * @param {express.Response} response Express response
@@ -161,7 +152,6 @@ async function sendClaudeRequest(request, response) {
161152 'x-api-key': apiKey,
162153 ...additionalHeaders,
163154 },
164- timeout: 0,
165155 });
166156
167157 if (request.body.stream) {
@@ -174,6 +164,7 @@ async function sendClaudeRequest(request, response) {
174164 return response.status(generateResponse.status).send({ error: true });
175165 }
176166
167+ /** @type {any} */
177168 const generateResponseJson = await generateResponse.json();
178169 const responseText = generateResponseJson?.content?.[0]?.text || '';
179170 console.log('Claude response:', generateResponseJson);
@@ -221,7 +212,6 @@ async function sendScaleRequest(request, response) {
221212 'Content-Type': 'application/json',
222213 'Authorization': `Basic ${apiKey}`,
223214 },
224- timeout: 0,
225215 });
226216
227217 if (!generateResponse.ok) {
@@ -229,6 +219,7 @@ async function sendScaleRequest(request, response) {
229219 return response.status(500).send({ error: true });
230220 }
231221
222+ /** @type {any} */
232223 const generateResponseJson = await generateResponse.json();
233224 console.log('Scale response:', generateResponseJson);
234225
@@ -337,14 +328,13 @@ async function sendMakerSuiteRequest(request, response) {
337328 ? (stream ? 'streamGenerateContent' : 'generateContent')
338329 : (isText ? 'generateText' : 'generateMessage');
339330
340331 const generateResponse = await fetch(`${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
341332 body: JSON.stringify(body),
342333 method: 'POST',
343334 headers: {
344335 'Content-Type': 'application/json',
345336 },
346337 signal: controller.signal,
347- timeout: 0,
348338 });
349339 // have to do this because of their busted ass streaming endpoint
350340 if (stream) {
@@ -363,6 +353,7 @@ async function sendMakerSuiteRequest(request, response) {
363353 return response.status(generateResponse.status).send({ error: true });
364354 }
365355
356+ /** @type {any} */
366357 const generateResponseJson = await generateResponse.json();
367358
368359 const candidates = generateResponseJson?.candidates;
@@ -626,7 +617,7 @@ async function sendCohereRequest(request, response) {
626617 }
627618}
628619
629620export const router = express.Router();
630621
631622router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
632623 if (!request.body) return response_getstatus_openai.sendStatus(400);
@@ -685,6 +676,7 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
685676 });
686677
687678 if (response.ok) {
679+ /** @type {any} */
688680 const data = await response.json();
689681 response_getstatus_openai.send(data);
690682
@@ -988,7 +980,6 @@ router.post('/generate', jsonParser, function (request, response) {
988980 },
989981 body: JSON.stringify(requestBody),
990982 signal: controller.signal,
991- timeout: 0,
992983 };
993984
994985 console.log(requestBody);
@@ -1014,6 +1005,7 @@ router.post('/generate', jsonParser, function (request, response) {
10141005 }
10151006
10161007 if (fetchResponse.ok) {
1008+ /** @type {any} */
10171009 let json = await fetchResponse.json();
10181010 response.send(json);
10191011 console.log(json);
@@ -1069,6 +1061,3 @@ router.post('/generate', jsonParser, function (request, response) {
10691061 }
10701062});
10711063
1072-module.exports = {
1073- router,
1074-};
src/endpoints/backends/kobold.js+10 -11
@@ -1,13 +1,13 @@
1-const express = require('express');
1+import fs from 'node:fs';
2-const fetch = require('node-fetch').default;
2+import express from 'express';
3-const fs = require('fs');
3+import fetch from 'node-fetch';
44
55constimport { jsonParser, urlencodedParser } =from require('../../express-common.js');
66constimport { forwardFetchResponse, delay } =from require('../../util.js');
77constimport { getOverrideHeaders, setAdditionalHeaders, setAdditionalHeadersByType } =from require('../../additional-headers.js');
88constimport { TEXTGEN_TYPES } =from require('../../constants.js');
99
1010export const router = express.Router();
1111
1212router.post('/generate', jsonParser, async function (request, response_generate) {
1313 if (!request.body) return response_generate.sendStatus(400);
@@ -96,7 +96,7 @@ router.post('/generate', jsonParser, async function (request, response_generate)
9696 for (let i = 0; i < MAX_RETRIES; i++) {
9797 try {
9898 const url = request.body.streaming ? `${request.body.api_server}/extra/generate/stream` : `${request.body.api_server}/v1/generate`;
9999 const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
100100
101101 if (request.body.streaming) {
102102 // Pipe remote SSE stream to Express response
@@ -156,6 +156,7 @@ router.post('/status', jsonParser, async function (request, response) {
156156
157157 const result = {};
158158
159+ /** @type {any} */
159160 const [koboldUnitedResponse, koboldExtraResponse, koboldModelResponse] = await Promise.all([
160161 // We catch errors both from the response not having a successful HTTP status and from JSON parsing failing
161162
@@ -237,5 +238,3 @@ router.post('/transcribe-audio', urlencodedParser, async function (request, resp
237238 response.status(500).send('Internal server error');
238239 }
239240});
240-
241-module.exports = { router };
src/endpoints/backends/scale-alt.js+6 -9
@@ -1,11 +1,10 @@
11constimport express =from require('express');
22constimport fetch =from require('node-fetch').default;
33
44constimport { jsonParser } =from require('../../express-common.js');
5+import { readSecret, SECRET_KEYS } from '../secrets.js';
56
6-const { readSecret, SECRET_KEYS } = require('../secrets');
7+export const router = express.Router();
7-
8-const router = express.Router();
98
109router.post('/generate', jsonParser, async function (request, response) {
1110 if (!request.body) return response.sendStatus(400);
@@ -71,7 +70,6 @@ router.post('/generate', jsonParser, async function (request, response) {
7170 'Content-Type': 'application/json',
7271 'cookie': `_jwt=${cookie}`,
7372 },
74- timeout: 0,
7573 body: JSON.stringify(body),
7674 });
7775
@@ -81,6 +79,7 @@ router.post('/generate', jsonParser, async function (request, response) {
8179 return response.status(500).send({ error: { message: result.statusText } });
8280 }
8381
82+ /** @type {any} */
8483 const data = await result.json();
8584 const output = data?.result?.data?.json?.outputs?.[0] || '';
8685
@@ -97,5 +96,3 @@ router.post('/generate', jsonParser, async function (request, response) {
9796 return response.sendStatus(500);
9897 }
9998});
100-
101-module.exports = { router };
src/endpoints/backends/text-completions.js+31 -19
@@ -1,14 +1,23 @@
1-const express = require('express');
1+import { Readable } from 'node:stream';
22constimport fetch =from require('node-fetch').default;
33constimport _express =from require('lodashexpress');
4-const Readable = require('stream').Readable;
4+import _ from 'lodash';
55
66constimport { jsonParser } =from require('../../express-common.js');
7-const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants');
7+import {
8-const { forwardFetchResponse, trimV1, getConfigValue } = require('../../util');
8+ TEXTGEN_TYPES,
9-const { setAdditionalHeaders } = require('../../additional-headers');
9+ TOGETHERAI_KEYS,
10-
10+ OLLAMA_KEYS,
11-const router = express.Router();
11+ INFERMATICAI_KEYS,
12+ OPENROUTER_KEYS,
13+ VLLM_KEYS,
14+ DREAMGEN_KEYS,
15+ FEATHERLESS_KEYS,
16+} from '../../constants.js';
17+import { forwardFetchResponse, trimV1, getConfigValue } from '../../util.js';
18+import { setAdditionalHeaders } from '../../additional-headers.js';
19+
20+export const router = express.Router();
1221
1322/**
1423 * Special boy's steaming routine. Wrap this abomination into proper SSE stream.
@@ -19,6 +28,10 @@ const router = express.Router();
1928 */
2029async function parseOllamaStream(jsonStream, request, response) {
2130 try {
31+ if (!jsonStream.body) {
32+ throw new Error('No body in the response');
33+ }
34+
2235 let partialData = '';
2336 jsonStream.body.on('data', (data) => {
2437 const chunk = data.toString();
@@ -144,6 +157,7 @@ router.post('/status', jsonParser, async function (request, response) {
144157 return response.status(400);
145158 }
146159
160+ /** @type {any} */
147161 let data = await modelsReply.json();
148162
149163 if (request.body.legacy_api) {
@@ -181,6 +195,7 @@ router.post('/status', jsonParser, async function (request, response) {
181195 const modelInfoReply = await fetch(modelInfoUrl, args);
182196
183197 if (modelInfoReply.ok) {
198+ /** @type {any} */
184199 const modelInfo = await modelInfoReply.json();
185200 console.log('Ooba model info:', modelInfo);
186201
@@ -197,6 +212,7 @@ router.post('/status', jsonParser, async function (request, response) {
197212 const modelInfoReply = await fetch(modelInfoUrl, args);
198213
199214 if (modelInfoReply.ok) {
215+ /** @type {any} */
200216 const modelInfo = await modelInfoReply.json();
201217 console.log('Tabby model info:', modelInfo);
202218
@@ -350,6 +366,7 @@ router.post('/generate', jsonParser, async function (request, response) {
350366 const completionsReply = await fetch(url, args);
351367
352368 if (completionsReply.ok) {
369+ /** @type {any} */
353370 const data = await completionsReply.json();
354371 console.log('Endpoint response:', data);
355372
@@ -406,7 +423,6 @@ ollama.post('/download', jsonParser, async function (request, response) {
406423 name: name,
407424 stream: false,
408425 }),
409- timeout: 0,
410426 });
411427
412428 if (!fetchResponse.ok) {
@@ -439,7 +455,6 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
439455 images: [request.body.image],
440456 stream: false,
441457 }),
442- timeout: 0,
443458 });
444459
445460 if (!fetchResponse.ok) {
@@ -447,6 +462,7 @@ ollama.post('/caption-image', jsonParser, async function (request, response) {
447462 return response.status(500).send({ error: true });
448463 }
449464
465+ /** @type {any} */
450466 const data = await fetchResponse.json();
451467 console.log('Ollama caption response:', data);
452468
@@ -478,7 +494,6 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
478494 const fetchResponse = await fetch(`${baseUrl}/completion`, {
479495 method: 'POST',
480496 headers: { 'Content-Type': 'application/json' },
481- timeout: 0,
482497 body: JSON.stringify({
483498 prompt: `USER:[img-1]${String(request.body.prompt).trim()}\nASSISTANT:`,
484499 image_data: [{ data: request.body.image, id: 1 }],
@@ -493,6 +508,7 @@ llamacpp.post('/caption-image', jsonParser, async function (request, response) {
493508 return response.status(500).send({ error: true });
494509 }
495510
511+ /** @type {any} */
496512 const data = await fetchResponse.json();
497513 console.log('LlamaCpp caption response:', data);
498514
@@ -522,7 +538,6 @@ llamacpp.post('/props', jsonParser, async function (request, response) {
522538
523539 const fetchResponse = await fetch(`${baseUrl}/props`, {
524540 method: 'GET',
525- timeout: 0,
526541 });
527542
528543 if (!fetchResponse.ok) {
@@ -557,7 +572,6 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
557572 if (request.body.action === 'info') {
558573 fetchResponse = await fetch(`${baseUrl}/slots`, {
559574 method: 'GET',
560- timeout: 0,
561575 });
562576 } else {
563577 if (!/^\d+$/.test(request.body.id_slot)) {
@@ -570,7 +584,6 @@ llamacpp.post('/slots', jsonParser, async function (request, response) {
570584 fetchResponse = await fetch(`${baseUrl}/slots/${request.body.id_slot}?action=${request.body.action}`, {
571585 method: 'POST',
572586 headers: { 'Content-Type': 'application/json' },
573- timeout: 0,
574587 body: JSON.stringify({
575588 filename: request.body.action !== 'erase' ? `${request.body.filename}` : undefined,
576589 }),
@@ -614,6 +627,7 @@ tabby.post('/download', jsonParser, async function (request, response) {
614627 });
615628
616629 if (permissionResponse.ok) {
630+ /** @type {any} */
617631 const permissionJson = await permissionResponse.json();
618632
619633 if (permissionJson['permission'] !== 'admin') {
@@ -641,5 +655,3 @@ tabby.post('/download', jsonParser, async function (request, response) {
641655router.use('/ollama', ollama);
642656router.use('/llamacpp', llamacpp);
643657router.use('/tabby', tabby);
644-
645-module.exports = { router };
src/endpoints/backgrounds.js+9 -10
@@ -1,13 +1,14 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
4-const sanitize = require('sanitize-filename');
53
6-const { jsonParser, urlencodedParser } = require('../express-common');
4+import express from 'express';
7-const { invalidateThumbnail } = require('./thumbnails');
5+import sanitize from 'sanitize-filename';
8-const { getImages } = require('../util');
96
10-const router = express.Router();
7+import { jsonParser, urlencodedParser } from '../express-common.js';
8+import { invalidateThumbnail } from './thumbnails.js';
9+import { getImages } from '../util.js';
10+
11+export const router = express.Router();
1112
1213router.post('/all', jsonParser, function (request, response) {
1314 var images = getImages(request.user.directories.backgrounds);
@@ -72,5 +73,3 @@ router.post('/upload', urlencodedParser, function (request, response) {
7273 response.sendStatus(500);
7374 }
7475});
75-
76-module.exports = { router };
src/endpoints/caption.js+6 -8
@@ -1,23 +1,23 @@
11constimport express =from require('express');
22constimport { jsonParser } =from require('../express-common.js');
3+import { getPipeline, getRawImage } from '../transformers.mjs';
34
45const TASK = 'image-to-text';
56
67export const router = express.Router();
78
89router.post('/', jsonParser, async (req, res) => {
910 try {
1011 const { image } = req.body;
1112
1213 const modulerawImage = await importgetRawImage('../transformers.mjs'image);
13- const rawImage = await module.default.getRawImage(image);
1414
1515 if (!rawImage) {
1616 console.log('Failed to parse captioned image');
1717 return res.sendStatus(400);
1818 }
1919
2020 const pipe = await module.default.getPipeline(TASK);
2121 const result = await pipe(rawImage);
2222 const text = result[0].generated_text;
2323 console.log('Image caption:', text);
@@ -28,5 +28,3 @@ router.post('/', jsonParser, async (req, res) => {
2828 return res.sendStatus(500);
2929 }
3030});
31-
32-module.exports = { router };
src/endpoints/characters.js+31 -31
@@ -1,24 +1,25 @@
11constimport path =from require('node:path');
22constimport fs =from require('node:fs');
3-const fsPromises = require('fs').promises;
3+import { promises as fsPromises } from 'node:fs';
44constimport readline =from require('node:readline');
5-const express = require('express');
5+import { Buffer } from 'node:buffer';
6-const sanitize = require('sanitize-filename');
6+
7-const writeFileAtomicSync = require('write-file-atomic').sync;
7+import express from 'express';
8-const yaml = require('yaml');
8+import sanitize from 'sanitize-filename';
9-const _ = require('lodash');
9+import { sync as writeFileAtomicSync } from 'write-file-atomic';
10-const mime = require('mime-types');
10+import yaml from 'yaml';
11-
11+import _ from 'lodash';
12-const jimp = require('jimp');
12+import mime from 'mime-types';
13-
13+import jimp from 'jimp';
14-const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
14+
1515constimport { jsonParserAVATAR_WIDTH, urlencodedParserAVATAR_HEIGHT } =from require('../express-commonconstants.js');
1616constimport { deepMerge, humanizedISO8601DateTime, tryParsejsonParser, extractFileFromZipBufferurlencodedParser } =from require('../utilexpress-common.js');
1717constimport { TavernCardValidatordeepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer } =from require('../validator/TavernCardValidatorutil.js');
1818constimport characterCardParser{ =TavernCardValidator require(} from '../character-card-parservalidator/TavernCardValidator.js');
1919constimport { readWorldInfoFileparse, write } =from require('../worldinfocharacter-card-parser.js');
2020constimport { invalidateThumbnailreadWorldInfoFile } =from require('./thumbnailsworldinfo.js');
2121constimport { importRisuSpritesinvalidateThumbnail } =from require('./spritesthumbnails.js');
22+import { importRisuSprites } from './sprites.js';
2223const defaultAvatarPath = './public/img/ai4.png';
2324
2425// KV-store for parsed character data
@@ -37,7 +38,7 @@ async function readCharacterData(inputFile, inputFormat = 'png') {
3738 return characterDataCache.get(cacheKey);
3839 }
3940
4041 const result = characterCardParser.parse(inputFile, inputFormat);
4142 characterDataCache.set(cacheKey, result);
4243 return result;
4344}
@@ -76,7 +77,7 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u
7677 const inputImage = await getInputImage();
7778
7879 // Get the chunks
7980 const outputImage = characterCardParser.write(inputImage, data);
8081 const outputImagePath = path.join(request.user.directories.characters, `${outputFile}.png`);
8182
8283 writeFileAtomicSync(outputImagePath, outputImage);
@@ -188,7 +189,7 @@ const calculateDataSize = (data) => {
188189 * processCharacter - Process a given character, read its data and calculate its statistics.
189190 *
190191 * @param {string} item The name of the character.
191192 * @param {import('../users.js').UserDirectoryList} directories User directories
192193 * @return {Promise<object>} A Promise that resolves when the character processing is done.
193194 */
194195const processCharacter = async (item, directories) => {
@@ -232,7 +233,7 @@ const processCharacter = async (item, directories) => {
232233/**
233234 * Convert a character object to Spec V2 format.
234235 * @param {object} jsonObject Character object
235236 * @param {import('../users.js').UserDirectoryList} directories User directories
236237 * @param {boolean} hoistDate Will set the chat and create_date fields to the current date if they are missing
237238 * @returns {object} Character object in Spec V2 format
238239 */
@@ -252,7 +253,7 @@ function getCharaCardV2(jsonObject, directories, hoistDate = true) {
252253/**
253254 * Convert a character object to Spec V2 format.
254255 * @param {object} char Character object
255256 * @param {import('../users.js').UserDirectoryList} directories User directories
256257 * @returns {object} Character object in Spec V2 format
257258 */
258259function convertToV2(char, directories) {
@@ -342,7 +343,7 @@ function readFromV2(char) {
342343/**
343344 * Format character data to Spec V2 format.
344345 * @param {object} data Character data
345346 * @param {import('../users.js').UserDirectoryList} directories User directories
346347 * @returns
347348 */
348349function charaFormatData(data, directories) {
@@ -467,6 +468,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
467468 position: entry.position == 0 ? 'before_char' : 'after_char',
468469 use_regex: true, // ST keys are always regex
469470 extensions: {
471+ ...entry.extensions,
470472 position: entry.position,
471473 exclude_recursion: entry.excludeRecursion,
472474 display_index: entry.displayIndex,
@@ -715,7 +717,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
715717 return '';
716718}
717719
718720export const router = express.Router();
719721
720722router.post('/create', urlencodedParser, async function (request, response) {
721723 try {
@@ -1081,7 +1083,7 @@ router.post('/chats', jsonParser, async function (request, response) {
10811083/**
10821084 * Gets the name for the uploaded PNG file.
10831085 * @param {string} file File name
10841086 * @param {import('../users.js').UserDirectoryList} directories User directories
10851087 * @returns {string} - The name for the uploaded PNG file
10861088 */
10871089function getPngName(file, directories) {
@@ -1231,5 +1233,3 @@ router.post('/export', jsonParser, async function (request, response) {
12311233 response.sendStatus(500);
12321234 }
12331235});
1234-
1235-module.exports = { router };
src/endpoints/chats.js+12 -12
@@ -1,13 +1,15 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
33constimport readline =from require('node:readline');
4-const express = require('express');
4+import process from 'node:process';
5-const sanitize = require('sanitize-filename');
6-const writeFileAtomicSync = require('write-file-atomic').sync;
7-const _ = require('lodash');
85
9-const { jsonParser, urlencodedParser } = require('../express-common');
6+import express from 'express';
10-const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util');
7+import sanitize from 'sanitize-filename';
8+import { sync as writeFileAtomicSync } from 'write-file-atomic';
9+import _ from 'lodash';
10+
11+import { jsonParser, urlencodedParser } from '../express-common.js';
12+import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } from '../util.js';
1113
1214/**
1315 * Saves a chat to the backups directory.
@@ -188,7 +190,7 @@ function flattenChubChat(userName, characterName, lines) {
188190 return (lines ?? []).map(convert).join('\n');
189191}
190192
191193export const router = express.Router();
192194
193195router.post('/save', jsonParser, function (request, response) {
194196 try {
@@ -514,5 +516,3 @@ router.post('/group/save', jsonParser, (request, response) => {
514516 getBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData);
515517 return response.send({ ok: true });
516518});
517-
518-module.exports = { router };
src/endpoints/classify.js+7 -9
@@ -1,9 +1,11 @@
11constimport express =from require('express');
2-const { jsonParser } = require('../express-common');
2+
3+import { getPipeline } from '../transformers.mjs';
4+import { jsonParser } from '../express-common.js';
35
46const TASK = 'text-classification';
57
68export const router = express.Router();
79
810/**
911 * @type {Map<string, object>} Cache for classification results
@@ -12,8 +14,7 @@ const cacheObject = new Map();
1214
1315router.post('/labels', jsonParser, async (req, res) => {
1416 try {
1517 const modulepipe = await importgetPipeline('../transformers.mjs'TASK);
16- const pipe = await module.default.getPipeline(TASK);
1718 const result = Object.keys(pipe.model.config.label2id);
1819 return res.json({ labels: result });
1920 } catch (error) {
@@ -35,8 +36,7 @@ router.post('/', jsonParser, async (req, res) => {
3536 if (cacheObject.has(text)) {
3637 return cacheObject.get(text);
3738 } else {
3839 const modulepipe = await importgetPipeline('../transformers.mjs'TASK);
39- const pipe = await module.default.getPipeline(TASK);
4040 const result = await pipe(text, { topk: 5 });
4141 result.sort((a, b) => b.score - a.score);
4242 cacheObject.set(text, result);
@@ -54,5 +54,3 @@ router.post('/', jsonParser, async (req, res) => {
5454 return res.sendStatus(500);
5555 }
5656});
57-
58-module.exports = { router };
src/endpoints/content-manager.js+27 -29
@@ -1,16 +1,21 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+import process from 'node:process';
4-const fetch = require('node-fetch').default;
4+import { Buffer } from 'node:buffer';
5-const sanitize = require('sanitize-filename');
5+
6-const { getConfigValue, color } = require('../util');
6+import express from 'express';
7-const { jsonParser } = require('../express-common');
7+import fetch from 'node-fetch';
8-const writeFileAtomicSync = require('write-file-atomic').sync;
8+import sanitize from 'sanitize-filename';
9+import { sync as writeFileAtomicSync } from 'write-file-atomic';
10+
11+import { getConfigValue, color } from '../util.js';
12+import { jsonParser } from '../express-common.js';
13+import { write } from '../character-card-parser.js';
14+
915const contentDirectory = path.join(process.cwd(), 'default/content');
1016const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');
1117const contentIndexPath = path.join(contentDirectory, 'index.json');
1218const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
13-const characterCardParser = require('../character-card-parser.js');
1419
1520const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
1621
@@ -26,7 +31,7 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo
2631 * @typedef {string} ContentType
2732 * @enum {string}
2833 */
2934export const CONTENT_TYPES = {
3035 SETTINGS: 'settings',
3136 CHARACTER: 'character',
3237 SPRITES: 'sprites',
@@ -48,10 +53,10 @@ const CONTENT_TYPES = {
4853
4954/**
5055 * Gets the default presets from the content directory.
5156 * @param {import('../users.js').UserDirectoryList} directories User directories
5257 * @returns {object[]} Array of default presets
5358 */
5459export function getDefaultPresets(directories) {
5560 try {
5661 const contentIndex = getContentIndex();
5762 const presets = [];
@@ -76,7 +81,7 @@ function getDefaultPresets(directories) {
7681 * @param {string} filename Name of the file to get
7782 * @returns {object | null} JSON object or null if the file doesn't exist
7883 */
7984export function getDefaultPresetFile(filename) {
8085 try {
8186 const contentPath = path.join(contentDirectory, filename);
8287
@@ -95,7 +100,7 @@ function getDefaultPresetFile(filename) {
95100/**
96101 * Seeds content for a user.
97102 * @param {ContentItem[]} contentIndex Content index
98103 * @param {import('../users.js').UserDirectoryList} directories User directories
99104 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)
100105 * @returns {Promise<boolean>} Whether any content was added
101106 */
@@ -154,11 +159,11 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
154159
155160/**
156161 * Checks for new content and seeds it for all users.
157162 * @param {import('../users.js').UserDirectoryList[]} directoriesList List of user directories
158163 * @param {string[]} forceCategories List of categories to force check (even if content check is skipped)
159164 * @returns {Promise<void>}
160165 */
161166export async function checkForNewContent(directoriesList, forceCategories = []) {
162167 try {
163168 const contentCheckSkip = getConfigValue('skipContentCheck', false);
164169 if (contentCheckSkip && forceCategories?.length === 0) {
@@ -224,7 +229,7 @@ function getContentIndex() {
224229 * @param {'json'|'string'|'raw'} format Format of content
225230 * @returns {string[]|Buffer[]} Array of content
226231 */
227232export function getContentOfType(type, format) {
228233 const contentIndex = getContentIndex();
229234 const indexItems = contentIndex.filter((item) => item.type === type && item.folder);
230235 const files = [];
@@ -256,7 +261,7 @@ function getContentOfType(type, format) {
256261/**
257262 * Gets the target directory for the specified asset type.
258263 * @param {ContentType} type Asset type
259264 * @param {import('../users.js').UserDirectoryList} directories User directories
260265 * @returns {string | null} Target directory
261266 */
262267function getTargetByType(type, directories) {
@@ -375,6 +380,7 @@ async function downloadPygmalionCharacter(id) {
375380 throw new Error('Failed to download character');
376381 }
377382
383+ /** @type {any} */
378384 const jsonData = await result.json();
379385 const characterData = jsonData?.character;
380386
@@ -394,7 +400,7 @@ async function downloadPygmalionCharacter(id) {
394400 const avatarResult = await fetch(avatarUrl);
395401 const avatarBuffer = await avatarResult.buffer();
396402
397403 const cardBuffer = characterCardParser.write(avatarBuffer, JSON.stringify(characterData));
398404
399405 return {
400406 buffer: cardBuffer,
@@ -467,6 +473,7 @@ async function downloadJannyCharacter(uuid) {
467473 });
468474
469475 if (result.ok) {
476+ /** @type {any} */
470477 const downloadResult = await result.json();
471478 if (downloadResult.status === 'ok') {
472479 const imageResult = await fetch(downloadResult.downloadUrl);
@@ -618,7 +625,7 @@ function isHostWhitelisted(host) {
618625 return WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES.includes(host);
619626}
620627
621628export const router = express.Router();
622629
623630router.post('/importURL', jsonParser, async (request, response) => {
624631 if (!request.body.url) {
@@ -753,12 +760,3 @@ router.post('/importUUID', jsonParser, async (request, response) => {
753760 return response.sendStatus(500);
754761 }
755762});
756-
757-module.exports = {
758- CONTENT_TYPES,
759- checkForNewContent,
760- getDefaultPresets,
761- getDefaultPresetFile,
762- getContentOfType,
763- router,
764-};
src/endpoints/extensions.js+10 -10
@@ -1,10 +1,12 @@
11constimport path =from require('node:path');
22constimport fs =from require('node:fs');
3-const express = require('express');
3+
4-const { default: simpleGit } = require('simple-git');
4+import express from 'express';
55constimport sanitize =from require('sanitize-filename');
66constimport { PUBLIC_DIRECTORIESdefault as simpleGit } =from require('../constantssimple-git');
7-const { jsonParser } = require('../express-common');
7+
8+import { PUBLIC_DIRECTORIES } from '../constants.js';
9+import { jsonParser } from '../express-common.js';
810
911/**
1012 * This function extracts the extension information from the manifest file.
@@ -47,7 +49,7 @@ async function checkIfRepoIsUpToDate(extensionPath) {
4749 };
4850}
4951
5052export const router = express.Router();
5153
5254/**
5355 * HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
@@ -240,5 +242,3 @@ router.get('/discover', jsonParser, function (request, response) {
240242
241243 return response.send(extensions);
242244});
243-
244-module.exports = { router };
src/endpoints/files.js+12 -11
@@ -1,12 +1,15 @@
11constimport path =from require('node:path');
22constimport fs =from require('node:fs');
3-const writeFileSyncAtomic = require('write-file-atomic').sync;
3+
44constimport express =from require('express');
55constimport sanitize =from require('sanitize-filename');
6-const router = express.Router();
6+import { sync as writeFileSyncAtomic } from 'write-file-atomic';
7-const { validateAssetFileName } = require('./assets');
7+
88constimport { jsonParservalidateAssetFileName } =from require('../express-commonassets.js');
99constimport { clientRelativePathjsonParser } =from require('../utilexpress-common.js');
10+import { clientRelativePath } from '../util.js';
11+
12+export const router = express.Router();
1013
1114router.post('/sanitize-filename', jsonParser, async (request, response) => {
1215 try {
@@ -97,5 +100,3 @@ router.post('/verify', jsonParser, async (request, response) => {
97100 return response.sendStatus(500);
98101 }
99102});
100-
101-module.exports = { router };
src/endpoints/google.js+31 -8
@@ -1,12 +1,15 @@
11constimport { readSecret, SECRET_KEYSBuffer } =from require('./secretsnode:buffer');
22constimport fetch =from require('node-fetch').default;
33constimport express =from require('express');
4-const { jsonParser } = require('../express-common');
4+import { speak, languages } from 'google-translate-api-x';
5-const { GEMINI_SAFETY } = require('../constants');
5+
6+import { readSecret, SECRET_KEYS } from './secrets.js';
7+import { jsonParser } from '../express-common.js';
8+import { GEMINI_SAFETY } from '../constants.js';
69
710const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
811
912export const router = express.Router();
1013
1114router.post('/caption-image', jsonParser, async (request, response) => {
1215 try {
@@ -39,7 +42,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
3942 headers: {
4043 'Content-Type': 'application/json',
4144 },
42- timeout: 0,
4345 });
4446
4547 if (!result.ok) {
@@ -48,6 +50,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4850 return response.status(result.status).send({ error: true });
4951 }
5052
53+ /** @type {any} */
5154 const data = await result.json();
5255 console.log('Multimodal captioning response', data);
5356
@@ -68,4 +71,24 @@ router.post('/caption-image', jsonParser, async (request, response) => {
6871 }
6972});
7073
71-module.exports = { router };
74+router.post('/list-voices', (_, response) => {
75+ return response.json(languages);
76+});
77+
78+router.post('/generate-voice', jsonParser, async (request, response) => {
79+ try {
80+ const text = request.body.text;
81+ const voice = request.body.voice ?? 'en';
82+
83+ const result = await speak(text, { to: voice, forceBatch: false });
84+ const buffer = Array.isArray(result)
85+ ? Buffer.concat(result.map(x => new Uint8Array(Buffer.from(x.toString(), 'base64'))))
86+ : Buffer.from(result.toString(), 'base64');
87+
88+ response.setHeader('Content-Type', 'audio/mpeg');
89+ return response.send(buffer);
90+ } catch (error) {
91+ console.error('Google Translate TTS generation failed', error);
92+ response.status(500).send('Internal server error');
93+ }
94+});
src/endpoints/groups.js+9 -10
@@ -1,13 +1,14 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
4-const sanitize = require('sanitize-filename');
5-const writeFileAtomicSync = require('write-file-atomic').sync;
63
7-const { jsonParser } = require('../express-common');
4+import express from 'express';
8-const { humanizedISO8601DateTime } = require('../util');
5+import sanitize from 'sanitize-filename';
6+import { sync as writeFileAtomicSync } from 'write-file-atomic';
97
10-const router = express.Router();
8+import { jsonParser } from '../express-common.js';
9+import { humanizedISO8601DateTime } from '../util.js';
10+
11+export const router = express.Router();
1112
1213router.post('/all', jsonParser, (request, response) => {
1314 const groups = [];
@@ -131,5 +132,3 @@ router.post('/delete', jsonParser, async (request, response) => {
131132
132133 return response.send({ ok: true });
133134});
134-
135-module.exports = { router };
src/endpoints/horde.js+7 -9
@@ -1,14 +1,14 @@
11constimport fetch =from require('node-fetch').default;
22constimport express =from require('express');
33constimport { AIHorde, ModelGenerationInputStableSamplers, ModelInterrogationFormTypes, HordeAsyncRequestStates } =from require('@zeldafan0225/ai_horde');
44constimport { getVersion, delay, Cache } =from require('../util.js');
55constimport { readSecret, SECRET_KEYS } =from require('./secrets.js');
66constimport { jsonParser } =from require('../express-common.js');
77
88const ANONYMOUS_KEY = '0000000000';
99const HORDE_TEXT_MODEL_METADATA_URL = 'https://raw.githubusercontent.com/db0/AI-Horde-text-model-reference/main/db.json';
1010const cache = new Cache(60 * 1000);
1111export const router = express.Router();
1212
1313/**
1414 * Returns the AIHorde client agent.
@@ -403,5 +403,3 @@ router.post('/generate-image', jsonParser, async (request, response) => {
403403 return response.sendStatus(500);
404404 }
405405});
406-
407-module.exports = { router };
src/endpoints/images.js+10 -10
@@ -1,10 +1,12 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+import { Buffer } from 'node:buffer';
4-const sanitize = require('sanitize-filename');
54
6-const { jsonParser } = require('../express-common');
5+import express from 'express';
7-const { clientRelativePath, removeFileExtension, getImages } = require('../util');
6+import sanitize from 'sanitize-filename';
7+
8+import { jsonParser } from '../express-common.js';
9+import { clientRelativePath, removeFileExtension, getImages } from '../util.js';
810
911/**
1012 * Ensure the directory for the provided file path exists.
@@ -21,7 +23,7 @@ function ensureDirectoryExistence(filePath) {
2123 fs.mkdirSync(dirname);
2224}
2325
2426export const router = express.Router();
2527
2628/**
2729 * Endpoint to handle image uploads.
@@ -66,7 +68,7 @@ router.post('/upload', jsonParser, async (request, response) => {
6668
6769 ensureDirectoryExistence(pathToNewFile);
6870 const imageBuffer = Buffer.from(base64Data, 'base64');
6971 await fs.promises.writeFile(pathToNewFile, new Uint8Array(imageBuffer));
7072 response.send({ path: clientRelativePath(request.user.directories.root, pathToNewFile) });
7173 } catch (error) {
7274 console.log(error);
@@ -89,5 +91,3 @@ router.post('/list/:folder', (request, response) => {
8991 return response.status(500).send({ error: 'Unable to retrieve files' });
9092 }
9193});
92-
93-module.exports = { router };
src/endpoints/moving-ui.js+6 -8
@@ -1,11 +1,11 @@
11constimport path =from require('node:path');
22constimport express =from require('express');
33constimport sanitize =from require('sanitize-filename');
44constimport { sync as writeFileAtomicSync =} require(from 'write-file-atomic').sync;
55
66constimport { jsonParser } =from require('../express-common.js');
77
88export const router = express.Router();
99
1010router.post('/save', jsonParser, (request, response) => {
1111 if (!request.body || !request.body.name) {
@@ -17,5 +17,3 @@ router.post('/save', jsonParser, (request, response) => {
1717
1818 return response.sendStatus(200);
1919});
20-
21-module.exports = { router };
src/endpoints/novelai.js+13 -12
@@ -1,9 +1,12 @@
1-const fetch = require('node-fetch').default;
1+import util from 'node:util';
2-const express = require('express');
2+import { Buffer } from 'node:buffer';
3-const util = require('util');
3+
4-const { readSecret, SECRET_KEYS } = require('./secrets');
4+import fetch from 'node-fetch';
5-const { readAllChunks, extractFileFromZipBuffer, forwardFetchResponse } = require('../util');
5+import express from 'express';
6-const { jsonParser } = require('../express-common');
6+
7+import { readSecret, SECRET_KEYS } from './secrets.js';
8+import { readAllChunks, extractFileFromZipBuffer, forwardFetchResponse } from '../util.js';
9+import { jsonParser } from '../express-common.js';
710
811const API_NOVELAI = 'https://api.novelai.net';
912const TEXT_NOVELAI = 'https://text.novelai.net';
@@ -110,7 +113,7 @@ function getRepPenaltyWhitelist(model) {
110113 return null;
111114}
112115
113116export const router = express.Router();
114117
115118router.post('/status', jsonParser, async function (req, res) {
116119 if (!req.body) return res.sendStatus(400);
@@ -249,7 +252,7 @@ router.post('/generate', jsonParser, async function (req, res) {
249252 try {
250253 const baseURL = (req.body.model.includes('kayra') || req.body.model.includes('erato')) ? TEXT_NOVELAI : API_NOVELAI;
251254 const url = req.body.streaming ? `${baseURL}/ai/generate-stream` : `${baseURL}/ai/generate`;
252255 const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
253256
254257 if (req.body.streaming) {
255258 // Pipe remote SSE stream to Express response
@@ -271,6 +274,7 @@ router.post('/generate', jsonParser, async function (req, res) {
271274 return res.status(response.status).send({ error: { message } });
272275 }
273276
277+ /** @type {any} */
274278 const data = await response.json();
275279 console.log('NovelAI Output', data?.output);
276280 return res.send(data);
@@ -413,7 +417,6 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
413417 'Authorization': `Bearer ${token}`,
414418 'Accept': 'audio/mpeg',
415419 },
416- timeout: 0,
417420 });
418421
419422 if (!result.ok) {
@@ -423,7 +426,7 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
423426 }
424427
425428 const chunks = await readAllChunks(result.body);
426429 const buffer = Buffer.concat(chunks.map(chunk => new Uint8Array(chunk)));
427430 response.setHeader('Content-Type', 'audio/mpeg');
428431 return response.send(buffer);
429432 }
@@ -432,5 +435,3 @@ router.post('/generate-voice', jsonParser, async (request, response) => {
432435 return response.sendStatus(500);
433436 }
434437});
435-
436-module.exports = { router };
src/endpoints/openai.js+15 -15
@@ -1,14 +1,17 @@
1-const { readSecret, SECRET_KEYS } = require('./secrets');
1+import fs from 'node:fs';
2-const fetch = require('node-fetch').default;
2+import { Buffer } from 'node:buffer';
3-const express = require('express');
3+
44constimport FormDatafetch =from require('formnode-datafetch');
5-const fs = require('fs');
5+import FormData from 'form-data';
6-const { jsonParser, urlencodedParser } = require('../express-common');
6+import express from 'express';
7-const { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } = require('../util');
7+
88constimport { setAdditionalHeadersjsonParser, urlencodedParser } =from require('../additionalexpress-headerscommon.js');
99constimport { OPENROUTER_HEADERSgetConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } =from require('../constantsutil.js');
10-
10+import { setAdditionalHeaders } from '../additional-headers.js';
11-const router = express.Router();
11+import { readSecret, SECRET_KEYS } from './secrets.js';
12+import { OPENROUTER_HEADERS } from '../constants.js';
13+
14+export const router = express.Router();
1215
1316router.post('/caption-image', jsonParser, async (request, response) => {
1417 try {
@@ -151,7 +154,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
151154 ...headers,
152155 },
153156 body: JSON.stringify(body),
154- timeout: 0,
155157 });
156158
157159 if (!result.ok) {
@@ -160,6 +162,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
160162 return response.status(500).send(text);
161163 }
162164
165+ /** @type {any} */
163166 const data = await result.json();
164167 console.log('Multimodal captioning response', data);
165168 const caption = data?.choices[0]?.message?.content;
@@ -281,7 +284,6 @@ router.post('/generate-image', jsonParser, async (request, response) => {
281284 Authorization: `Bearer ${key}`,
282285 },
283286 body: JSON.stringify(request.body),
284- timeout: 0,
285287 });
286288
287289 if (!result.ok) {
@@ -341,5 +343,3 @@ custom.post('/generate-voice', jsonParser, async (request, response) => {
341343});
342344
343345router.use('/custom', custom);
344-
345-module.exports = { router };
src/endpoints/openrouter.js+3 -5
@@ -1,7 +1,7 @@
11constimport express =from require('express');
22constimport { jsonParser } =from require('../express-common.js');
33
44export const router = express.Router();
55const API_OPENROUTER = 'https://openrouter.ai/api/v1';
66
77router.post('/models/multimodal', jsonParser, async (_req, res) => {
@@ -28,5 +28,3 @@ router.post('/models/multimodal', jsonParser, async (_req, res) => {
2828 return res.sendStatus(500);
2929 }
3030});
31-
32-module.exports = { router };
src/endpoints/presets.js+11 -11
@@ -1,15 +1,17 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+
4-const sanitize = require('sanitize-filename');
4+import express from 'express';
5-const writeFileAtomicSync = require('write-file-atomic').sync;
5+import sanitize from 'sanitize-filename';
66constimport { getDefaultPresetFile,sync getDefaultPresetsas writeFileAtomicSync } =from require('./contentwrite-managerfile-atomic');
7-const { jsonParser } = require('../express-common');
7+
8+import { getDefaultPresetFile, getDefaultPresets } from './content-manager.js';
9+import { jsonParser } from '../express-common.js';
810
911/**
1012 * Gets the folder and extension for the preset settings based on the API source ID.
1113 * @param {string} apiId API source ID
1214 * @param {import('../users.js').UserDirectoryList} directories User directories
1315 * @returns {object} Object containing the folder and extension for the preset settings
1416 */
1517function getPresetSettingsByAPI(apiId, directories) {
@@ -34,7 +36,7 @@ function getPresetSettingsByAPI(apiId, directories) {
3436 }
3537}
3638
3739export const router = express.Router();
3840
3941router.post('/save', jsonParser, function (request, response) {
4042 const name = sanitize(request.body.name);
@@ -127,5 +129,3 @@ router.post('/delete-openai', jsonParser, function (request, response) {
127129
128130 return response.send({ error: true });
129131});
130-
131-module.exports = { router };
src/endpoints/quick-replies.js+8 -9
@@ -1,12 +1,13 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
4-const sanitize = require('sanitize-filename');
5-const writeFileAtomicSync = require('write-file-atomic').sync;
63
7-const { jsonParser } = require('../express-common');
4+import express from 'express';
5+import sanitize from 'sanitize-filename';
6+import { sync as writeFileAtomicSync } from 'write-file-atomic';
87
9-const router = express.Router();
8+import { jsonParser } from '../express-common.js';
9+
10+export const router = express.Router();
1011
1112router.post('/save', jsonParser, (request, response) => {
1213 if (!request.body || !request.body.name) {
@@ -31,5 +32,3 @@ router.post('/delete', jsonParser, (request, response) => {
3132
3233 return response.sendStatus(200);
3334});
34-
35-module.exports = { router };
src/endpoints/search.js+8 -9
@@ -1,9 +1,11 @@
11constimport fetch =from require('node-fetch').default;
22constimport express =from require('express');
3-const { readSecret, SECRET_KEYS } = require('./secrets');
4-const { jsonParser } = require('../express-common');
53
6-const router = express.Router();
4+import { decode } from 'html-entities';
5+import { readSecret, SECRET_KEYS } from './secrets.js';
6+import { jsonParser } from '../express-common.js';
7+
8+export const router = express.Router();
79
810// Cosplay as Chrome
911const visitHeaders = {
@@ -29,7 +31,6 @@ const visitHeaders = {
2931 * @returns {Promise<string>} Transcript text
3032 */
3133async function extractTranscript(videoPageBody, lang) {
32- const he = require('he');
3334 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
3435 const splittedHTML = videoPageBody.split('"captions":');
3536
@@ -84,7 +85,7 @@ async function extractTranscript(videoPageBody, lang) {
8485 lang: lang ?? captions.captionTracks[0].languageCode,
8586 }));
8687 // The text is double-encoded
8788 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
8889 return transcriptText;
8990}
9091
@@ -263,5 +264,3 @@ router.post('/visit', jsonParser, async (request, response) => {
263264 return response.sendStatus(500);
264265 }
265266});
266-
267-module.exports = { router };
src/endpoints/secrets.js+21 -30
@@ -1,12 +1,13 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+
4-const { getConfigValue } = require('../util');
4+import express from 'express';
55constimport { sync as writeFileAtomicSync =} require(from 'write-file-atomic').sync;
66constimport { jsonParsergetConfigValue } =from require('../express-commonutil.js');
7-
7+import { jsonParser } from '../express-common.js';
8-const SECRETS_FILE = 'secrets.json';
8+
9-const SECRET_KEYS = {
9+export const SECRETS_FILE = 'secrets.json';
10+export const SECRET_KEYS = {
1011 HORDE: 'api_key_horde',
1112 MANCER: 'api_key_mancer',
1213 VLLM: 'api_key_vllm',
@@ -58,11 +59,11 @@ const EXPORTABLE_KEYS = [
5859
5960/**
6061 * Writes a secret to the secrets file
6162 * @param {import('../users.js').UserDirectoryList} directories User directories
6263 * @param {string} key Secret key
6364 * @param {string} value Secret value
6465 */
6566export function writeSecret(directories, key, value) {
6667 const filePath = path.join(directories.root, SECRETS_FILE);
6768
6869 if (!fs.existsSync(filePath)) {
@@ -78,11 +79,11 @@ function writeSecret(directories, key, value) {
7879
7980/**
8081 * Deletes a secret from the secrets file
8182 * @param {import('../users.js').UserDirectoryList} directories User directories
8283 * @param {string} key Secret key
8384 * @returns
8485 */
8586export function deleteSecret(directories, key) {
8687 const filePath = path.join(directories.root, SECRETS_FILE);
8788
8889 if (!fs.existsSync(filePath)) {
@@ -97,11 +98,11 @@ function deleteSecret(directories, key) {
9798
9899/**
99100 * Reads a secret from the secrets file
100101 * @param {import('../users.js').UserDirectoryList} directories User directories
101102 * @param {string} key Secret key
102103 * @returns {string} Secret value
103104 */
104105export function readSecret(directories, key) {
105106 const filePath = path.join(directories.root, SECRETS_FILE);
106107
107108 if (!fs.existsSync(filePath)) {
@@ -115,10 +116,10 @@ function readSecret(directories, key) {
115116
116117/**
117118 * Reads the secret state from the secrets file
118119 * @param {import('../users.js').UserDirectoryList} directories User directories
119120 * @returns {object} Secret state
120121 */
121122export function readSecretState(directories) {
122123 const filePath = path.join(directories.root, SECRETS_FILE);
123124
124125 if (!fs.existsSync(filePath)) {
@@ -138,10 +139,10 @@ function readSecretState(directories) {
138139
139140/**
140141 * Reads all secrets from the secrets file
141142 * @param {import('../users.js').UserDirectoryList} directories User directories
142143 * @returns {Record<string, string> | undefined} Secrets
143144 */
144145export function getAllSecrets(directories) {
145146 const filePath = path.join(directories.root, SECRETS_FILE);
146147
147148 if (!fs.existsSync(filePath)) {
@@ -154,7 +155,7 @@ function getAllSecrets(directories) {
154155 return secrets;
155156}
156157
157158export const router = express.Router();
158159
159160router.post('/write', jsonParser, (request, response) => {
160161 const key = request.body.key;
@@ -218,13 +219,3 @@ router.post('/find', jsonParser, (request, response) => {
218219 return response.sendStatus(500);
219220 }
220221});
221-
222-module.exports = {
223- writeSecret,
224- readSecret,
225- deleteSecret,
226- readSecretState,
227- getAllSecrets,
228- SECRET_KEYS,
229- router,
230-};
src/endpoints/settings.js+13 -13
@@ -1,12 +1,14 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+
44constimport _express =from require('lodashexpress');
5-const writeFileAtomicSync = require('write-file-atomic').sync;
5+import _ from 'lodash';
6-const { SETTINGS_FILE } = require('../constants');
6+import { sync as writeFileAtomicSync } from 'write-file-atomic';
7-const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');
7+
88constimport { jsonParserSETTINGS_FILE } =from require('../express-commonconstants.js');
99constimport { getAllUserHandlesgetConfigValue, getUserDirectoriesgenerateTimestamp, removeOldBackups } =from require('../usersutil.js');
10+import { jsonParser } from '../express-common.js';
11+import { getAllUserHandles, getUserDirectories } from '../users.js';
1012
1113const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
1214const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true);
@@ -190,7 +192,7 @@ function getLatestBackup(handle) {
190192 return path.join(userDirectories.backups, latestBackup);
191193}
192194
193195export const router = express.Router();
194196
195197router.post('/save', jsonParser, function (request, response) {
196198 try {
@@ -357,8 +359,6 @@ router.post('/restore-snapshot', jsonParser, async (request, response) => {
357359/**
358360 * Initializes the settings endpoint
359361 */
360362export async function init() {
361363 await backupSettings();
362364}
363-
364-module.exports = { router, init };
src/endpoints/speech.js+8 -11
@@ -1,7 +1,10 @@
1-const express = require('express');
1+import { Buffer } from 'node:buffer';
2-const { jsonParser } = require('../express-common');
2+import express from 'express';
3+import wavefile from 'wavefile';
4+import { jsonParser } from '../express-common.js';
5+import { getPipeline } from '../transformers.mjs';
36
47export const router = express.Router();
58
69/**
710 * Gets the audio data from a base64-encoded audio file.
@@ -9,7 +12,6 @@ const router = express.Router();
912 * @returns {Float64Array} Audio data
1013 */
1114function getWaveFile(audio) {
12- const wavefile = require('wavefile');
1315 const wav = new wavefile.WaveFile();
1416 wav.fromDataURI(audio);
1517 wav.toBitDepth('32f');
@@ -36,8 +38,7 @@ router.post('/recognize', jsonParser, async (req, res) => {
3638 try {
3739 const TASK = 'automatic-speech-recognition';
3840 const { model, audio, lang } = req.body;
3941 const modulepipe = await importgetPipeline('../transformers.mjs'TASK, model);
40- const pipe = await module.default.getPipeline(TASK, model);
4142 const wav = getWaveFile(audio);
4243 const start = performance.now();
4344 const result = await pipe(wav, { language: lang || null, task: 'transcribe' });
@@ -54,11 +55,9 @@ router.post('/recognize', jsonParser, async (req, res) => {
5455
5556router.post('/synthesize', jsonParser, async (req, res) => {
5657 try {
57- const wavefile = require('wavefile');
5858 const TASK = 'text-to-speech';
5959 const { text, model, speaker } = req.body;
6060 const modulepipe = await importgetPipeline('../transformers.mjs'TASK, model);
61- const pipe = await module.default.getPipeline(TASK, model);
6261 const speaker_embeddings = speaker
6362 ? new Float32Array(new Uint8Array(Buffer.from(speaker.startsWith('data:') ? speaker.split(',')[1] : speaker, 'base64')).buffer)
6463 : null;
@@ -78,5 +77,3 @@ router.post('/synthesize', jsonParser, async (req, res) => {
7877 return res.sendStatus(500);
7978 }
8079});
81-
82-module.exports = { router };
src/endpoints/sprites.js+13 -17
@@ -1,16 +1,17 @@
1+import fs from 'node:fs';
2+import path from 'node:path';
13
24constimport fsexpress =from require('fsexpress');
3-const path = require('path');
5+import mime from 'mime-types';
4-const express = require('express');
6+import sanitize from 'sanitize-filename';
5-const mime = require('mime-types');
7+import { sync as writeFileAtomicSync } from 'write-file-atomic';
6-const sanitize = require('sanitize-filename');
8+
7-const writeFileAtomicSync = require('write-file-atomic').sync;
9+import { getImageBuffers } from '../util.js';
810constimport { getImageBuffersjsonParser, urlencodedParser } =from require('../utilexpress-common.js');
9-const { jsonParser, urlencodedParser } = require('../express-common');
1011
1112/**
1213 * Gets the path to the sprites folder for the provided character name
1314 * @param {import('../users.js').UserDirectoryList} directories - User directories
1415 * @param {string} name - The name of the character
1516 * @param {boolean} isSubfolder - Whether the name contains a subfolder
1617 * @returns {string | null} The path to the sprites folder. Null if the name is invalid.
@@ -41,11 +42,11 @@ function getSpritesPath(directories, name, isSubfolder) {
4142 * Imports base64 encoded sprites from RisuAI character data.
4243 * The sprites are saved in the character's sprites folder.
4344 * The additionalAssets and emotions are removed from the data.
4445 * @param {import('../users.js').UserDirectoryList} directories User directories
4546 * @param {object} data RisuAI character data
4647 * @returns {void}
4748 */
4849export function importRisuSprites(directories, data) {
4950 try {
5051 const name = data?.data?.name;
5152 const risuData = data?.data?.extensions?.risuai;
@@ -106,7 +107,7 @@ function importRisuSprites(directories, data) {
106107 }
107108}
108109
109110export const router = express.Router();
110111
111112router.get('/get', jsonParser, function (request, response) {
112113 const name = String(request.query.name);
@@ -259,8 +260,3 @@ router.post('/upload', urlencodedParser, async (request, response) => {
259260 return response.sendStatus(500);
260261 }
261262});
262-
263-module.exports = {
264- router,
265- importRisuSprites,
266-};
src/endpoints/stable-diffusion.js+42 -28
@@ -1,13 +1,15 @@
1-const express = require('express');
1+import fs from 'node:fs';
2-const fetch = require('node-fetch').default;
2+import path from 'node:path';
3-const sanitize = require('sanitize-filename');
3+
4-const { getBasicAuthHeader, delay, getHexString } = require('../util.js');
4+import express from 'express';
5-const fs = require('fs');
5+import fetch from 'node-fetch';
6-const path = require('path');
6+import sanitize from 'sanitize-filename';
77constimport { sync as writeFileAtomicSync =} require(from 'write-file-atomic').sync;
8-const { jsonParser } = require('../express-common');
8+import FormData from 'form-data';
9-const { readSecret, SECRET_KEYS } = require('./secrets.js');
9+
10-const FormData = require('form-data');
10+import { getBasicAuthHeader, delay } from '../util.js';
11+import { jsonParser } from '../express-common.js';
12+import { readSecret, SECRET_KEYS } from './secrets.js';
1113
1214/**
1315 * Gets the comfy workflows.
@@ -21,7 +23,7 @@ function getComfyWorkflows(directories) {
2123 .sort(Intl.Collator().compare);
2224}
2325
2426export const router = express.Router();
2527
2628router.post('/ping', jsonParser, async (request, response) => {
2729 try {
@@ -63,6 +65,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
6365 throw new Error('SD WebUI returned an error.');
6466 }
6567
68+ /** @type {any} */
6669 const data = await result.json();
6770 const names = data.map(x => x.name);
6871 return names;
@@ -83,6 +86,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
8386 throw new Error('SD WebUI returned an error.');
8487 }
8588
89+ /** @type {any} */
8690 const data = await result.json();
8791 const names = data.map(x => x.name);
8892 return names;
@@ -116,6 +120,7 @@ router.post('/vaes', jsonParser, async (request, response) => {
116120 throw new Error('SD WebUI returned an error.');
117121 }
118122
123+ /** @type {any} */
119124 const data = await result.json();
120125 const names = data.map(x => x.model_name);
121126 return response.send(names);
@@ -141,6 +146,7 @@ router.post('/samplers', jsonParser, async (request, response) => {
141146 throw new Error('SD WebUI returned an error.');
142147 }
143148
149+ /** @type {any} */
144150 const data = await result.json();
145151 const names = data.map(x => x.name);
146152 return response.send(names);
@@ -167,6 +173,7 @@ router.post('/schedulers', jsonParser, async (request, response) => {
167173 throw new Error('SD WebUI returned an error.');
168174 }
169175
176+ /** @type {any} */
170177 const data = await result.json();
171178 const names = data.map(x => x.name);
172179 return response.send(names);
@@ -192,6 +199,7 @@ router.post('/models', jsonParser, async (request, response) => {
192199 throw new Error('SD WebUI returned an error.');
193200 }
194201
202+ /** @type {any} */
195203 const data = await result.json();
196204 const models = data.map(x => ({ value: x.title, text: x.title }));
197205 return response.send(models);
@@ -212,6 +220,7 @@ router.post('/get-model', jsonParser, async (request, response) => {
212220 'Authorization': getBasicAuthHeader(request.body.auth),
213221 },
214222 });
223+ /** @type {any} */
215224 const data = await result.json();
216225 return response.send(data['sd_model_checkpoint']);
217226 } catch (error) {
@@ -231,7 +240,6 @@ router.post('/set-model', jsonParser, async (request, response) => {
231240 headers: {
232241 'Authorization': getBasicAuthHeader(request.body.auth),
233242 },
234- timeout: 0,
235243 });
236244 const data = await result.json();
237245 return data;
@@ -251,7 +259,6 @@ router.post('/set-model', jsonParser, async (request, response) => {
251259 'Content-Type': 'application/json',
252260 'Authorization': getBasicAuthHeader(request.body.auth),
253261 },
254- timeout: 0,
255262 });
256263
257264 if (!result.ok) {
@@ -262,6 +269,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
262269 const CHECK_INTERVAL = 2000;
263270
264271 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
272+ /** @type {any} */
265273 const progressState = await getProgress();
266274
267275 const progress = progressState['progress'];
@@ -306,8 +314,6 @@ router.post('/generate', jsonParser, async (request, response) => {
306314 'Content-Type': 'application/json',
307315 'Authorization': getBasicAuthHeader(request.body.auth),
308316 },
309- timeout: 0,
310- // @ts-ignore
311317 signal: controller.signal,
312318 });
313319
@@ -343,6 +349,7 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => {
343349 // Vlad doesn't provide Latent Upscalers in the API, so we have to hardcode them here
344350 const latentUpscalers = ['Latent', 'Latent (antialiased)', 'Latent (bicubic)', 'Latent (bicubic antialiased)', 'Latent (nearest)', 'Latent (nearest-exact)'];
345351
352+ /** @type {any} */
346353 const data = await result.json();
347354 const names = data.map(x => x.name);
348355
@@ -385,6 +392,7 @@ comfy.post('/samplers', jsonParser, async (request, response) => {
385392 throw new Error('ComfyUI returned an error.');
386393 }
387394
395+ /** @type {any} */
388396 const data = await result.json();
389397 return response.send(data.KSampler.input.required.sampler_name[0]);
390398 } catch (error) {
@@ -402,6 +410,7 @@ comfy.post('/models', jsonParser, async (request, response) => {
402410 if (!result.ok) {
403411 throw new Error('ComfyUI returned an error.');
404412 }
413+ /** @type {any} */
405414 const data = await result.json();
406415 return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })));
407416 } catch (error) {
@@ -420,6 +429,7 @@ comfy.post('/schedulers', jsonParser, async (request, response) => {
420429 throw new Error('ComfyUI returned an error.');
421430 }
422431
432+ /** @type {any} */
423433 const data = await result.json();
424434 return response.send(data.KSampler.input.required.scheduler[0]);
425435 } catch (error) {
@@ -438,6 +448,7 @@ comfy.post('/vaes', jsonParser, async (request, response) => {
438448 throw new Error('ComfyUI returned an error.');
439449 }
440450
451+ /** @type {any} */
441452 const data = await result.json();
442453 return response.send(data.VAELoader.input.required.vae_name[0]);
443454 } catch (error) {
@@ -519,6 +530,7 @@ comfy.post('/generate', jsonParser, async (request, response) => {
519530 throw new Error('ComfyUI returned an error.');
520531 }
521532
533+ /** @type {any} */
522534 const data = await promptResult.json();
523535 const id = data.prompt_id;
524536 let item;
@@ -529,6 +541,7 @@ comfy.post('/generate', jsonParser, async (request, response) => {
529541 if (!result.ok) {
530542 throw new Error('ComfyUI returned an error.');
531543 }
544+ /** @type {any} */
532545 const history = await result.json();
533546 item = history[id];
534547 if (item) {
@@ -607,10 +620,9 @@ together.post('/generate', jsonParser, async (request, response) => {
607620
608621 console.log('TogetherAI request:', request.body);
609622
610623 const result = await fetch('https://api.together.xyz/apiv1/inferenceimages/generations', {
611624 method: 'POST',
612625 body: JSON.stringify({
613- request_type: 'image-model-inference',
614626 prompt: request.body.prompt,
615627 negative_prompt: request.body.negative_prompt,
616628 height: request.body.height,
@@ -620,8 +632,6 @@ together.post('/generate', jsonParser, async (request, response) => {
620632 n: 1,
621633 // Limited to 10000 on playground, works fine with more.
622634 seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000),
623- // Don't know if that's supposed to be random or not. It works either way.
624- sessionKey: getHexString(40),
625635 }),
626636 headers: {
627637 'Content-Type': 'application/json',
@@ -630,19 +640,23 @@ together.post('/generate', jsonParser, async (request, response) => {
630640 });
631641
632642 if (!result.ok) {
633643 console.log('TogetherAI returned an error.', { body: await result.text() });
634644 return response.sendStatus(500);
635645 }
636646
647+ /** @type {any} */
637648 const data = await result.json();
638649 console.log('TogetherAI response:', data);
639650
640- if (data.status !== 'finished') {
651+ const choice = data?.data?.[0];
641- console.log('TogetherAI job failed.');
652+ let b64_json = choice.b64_json;
642- return response.sendStatus(500);
653+
654+ if (!b64_json) {
655+ const buffer = await (await fetch(choice.url)).buffer();
656+ b64_json = buffer.toString('base64');
643657 }
644658
645659 return response.send({ format: 'jpg', data: b64_json });
646660 } catch (error) {
647661 console.log(error);
648662 return response.sendStatus(500);
@@ -679,6 +693,8 @@ drawthings.post('/get-model', jsonParser, async (request, response) => {
679693 const result = await fetch(url, {
680694 method: 'GET',
681695 });
696+
697+ /** @type {any} */
682698 const data = await result.json();
683699
684700 return response.send(data['model']);
@@ -696,6 +712,8 @@ drawthings.post('/get-upscaler', jsonParser, async (request, response) => {
696712 const result = await fetch(url, {
697713 method: 'GET',
698714 });
715+
716+ /** @type {any} */
699717 const data = await result.json();
700718
701719 return response.send(data['upscaler']);
@@ -724,7 +742,6 @@ drawthings.post('/generate', jsonParser, async (request, response) => {
724742 'Content-Type': 'application/json',
725743 'Authorization': auth,
726744 },
727- timeout: 0,
728745 });
729746
730747 if (!result.ok) {
@@ -846,7 +863,6 @@ stability.post('/generate', jsonParser, async (request, response) => {
846863 'Accept': 'image/*',
847864 },
848865 body: formData,
849- timeout: 0,
850866 });
851867
852868 if (!result.ok) {
@@ -993,5 +1009,3 @@ router.use('/pollinations', pollinations);
9931009router.use('/stability', stability);
9941010router.use('/blockentropy', blockentropy);
9951011router.use('/huggingface', huggingface);
996-
997-module.exports = { router };
src/endpoints/stats.js+12 -18
@@ -1,14 +1,15 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+import crypto from 'node:crypto';
4-const writeFileAtomic = require('write-file-atomic');
4+
55constimport cryptoexpress =from require('cryptoexpress');
6+import writeFileAtomic from 'write-file-atomic';
67
78const readFile = fs.promises.readFile;
89const readdir = fs.promises.readdir;
910
1011constimport { jsonParser } =from require('../express-common.js');
1112constimport { getAllUserHandles, getUserDirectories } =from require('../users.js');
1213
1314const STATS_FILE = 'stats.json';
1415
@@ -146,7 +147,7 @@ async function collectAndCreateStats(chatsPath, charactersPath) {
146147 * @param {string} chatsPath Path to the directory containing the chat files.
147148 * @param {string} charactersPath Path to the directory containing the character files.
148149 */
149150export async function recreateStats(handle, chatsPath, charactersPath) {
150151 console.log('Collecting and creating stats for user:', handle);
151152 const stats = await collectAndCreateStats(chatsPath, charactersPath);
152153 STATS.set(handle, stats);
@@ -157,7 +158,7 @@ async function recreateStats(handle, chatsPath, charactersPath) {
157158 * Loads the stats file into memory. If the file doesn't exist or is invalid,
158159 * initializes stats by collecting and creating them for each character.
159160 */
160161export async function init() {
161162 try {
162163 const userHandles = await getAllUserHandles();
163164 for (const handle of userHandles) {
@@ -209,7 +210,7 @@ async function saveStatsToFile() {
209210 * Attempts to save charStats to a file and then terminates the process.
210211 * If an error occurs during the file write, it logs the error before exiting.
211212 */
212213export async function onExit() {
213214 try {
214215 await saveStatsToFile();
215216 } catch (err) {
@@ -434,7 +435,7 @@ function calculateTotalGenTimeAndWordCount(
434435 };
435436}
436437
437438export const router = express.Router();
438439
439440/**
440441 * Handle a POST request to get the stats object
@@ -465,10 +466,3 @@ router.post('/update', jsonParser, function (request, response) {
465466 setCharStats(request.user.profile.handle, request.body);
466467 return response.sendStatus(200);
467468});
468-
469-module.exports = {
470- router,
471- recreateStats,
472- init,
473- onExit,
474-};
src/endpoints/themes.js+9 -9
@@ -1,11 +1,13 @@
1-const express = require('express');
1+import path from 'node:path';
2-const path = require('path');
2+import fs from 'node:fs';
3-const fs = require('fs');
4-const sanitize = require('sanitize-filename');
5-const writeFileAtomicSync = require('write-file-atomic').sync;
6-const { jsonParser } = require('../express-common');
73
8-const router = express.Router();
4+import express from 'express';
5+import sanitize from 'sanitize-filename';
6+import { sync as writeFileAtomicSync } from 'write-file-atomic';
7+
8+import { jsonParser } from '../express-common.js';
9+
10+export const router = express.Router();
911
1012router.post('/save', jsonParser, (request, response) => {
1113 if (!request.body || !request.body.name) {
@@ -36,5 +38,3 @@ router.post('/delete', jsonParser, function (request, response) {
3638 return response.sendStatus(500);
3739 }
3840});
39-
40-module.exports = { router };
src/endpoints/thumbnails.js+20 -24
@@ -1,14 +1,16 @@
11constimport fs =from require('node:fs');
2-const fsPromises = require('fs').promises;
2+import { promises as fsPromises } from 'node:fs';
33constimport path =from require('node:path');
4-const mime = require('mime-types');
4+
5-const express = require('express');
5+import mime from 'mime-types';
6-const sanitize = require('sanitize-filename');
6+import express from 'express';
7-const jimp = require('jimp');
7+import sanitize from 'sanitize-filename';
8-const writeFileAtomicSync = require('write-file-atomic').sync;
8+import jimp from 'jimp';
9-const { getAllUserHandles, getUserDirectories } = require('../users');
9+import { sync as writeFileAtomicSync } from 'write-file-atomic';
10-const { getConfigValue } = require('../util');
10+
1111constimport { jsonParsergetAllUserHandles, getUserDirectories } =from require('../express-commonusers.js');
12+import { getConfigValue } from '../util.js';
13+import { jsonParser } from '../express-common.js';
1214
1315const thumbnailsDisabled = getConfigValue('disableThumbnails', false);
1416const quality = getConfigValue('thumbnailsQuality', 95);
@@ -16,7 +18,7 @@ const pngFormat = getConfigValue('avatarThumbnailsPng', false);
1618
1719/**
1820 * Gets a path to thumbnail folder based on the type.
1921 * @param {import('../users.js').UserDirectoryList} directories User directories
2022 * @param {'bg' | 'avatar'} type Thumbnail type
2123 * @returns {string} Path to the thumbnails folder
2224 */
@@ -37,7 +39,7 @@ function getThumbnailFolder(directories, type) {
3739
3840/**
3941 * Gets a path to the original images folder based on the type.
4042 * @param {import('../users.js').UserDirectoryList} directories User directories
4143 * @param {'bg' | 'avatar'} type Thumbnail type
4244 * @returns {string} Path to the original images folder
4345 */
@@ -58,11 +60,11 @@ function getOriginalFolder(directories, type) {
5860
5961/**
6062 * Removes the generated thumbnail from the disk.
6163 * @param {import('../users.js').UserDirectoryList} directories User directories
6264 * @param {'bg' | 'avatar'} type Type of the thumbnail
6365 * @param {string} file Name of the file
6466 */
6567export function invalidateThumbnail(directories, type, file) {
6668 const folder = getThumbnailFolder(directories, type);
6769 if (folder === undefined) throw new Error('Invalid thumbnail type');
6870
@@ -75,7 +77,7 @@ function invalidateThumbnail(directories, type, file) {
7577
7678/**
7779 * Generates a thumbnail for the given file.
7880 * @param {import('../users.js').UserDirectoryList} directories User directories
7981 * @param {'bg' | 'avatar'} type Type of the thumbnail
8082 * @param {string} file Name of the file
8183 * @returns
@@ -141,7 +143,7 @@ async function generateThumbnail(directories, type, file) {
141143 * Ensures that the thumbnail cache for backgrounds is valid.
142144 * @returns {Promise<void>} Promise that resolves when the cache is validated
143145 */
144146export async function ensureThumbnailCache() {
145147 const userHandles = await getAllUserHandles();
146148 for (const handle of userHandles) {
147149 const directories = getUserDirectories(handle);
@@ -166,7 +168,7 @@ async function ensureThumbnailCache() {
166168 }
167169}
168170
169171export const router = express.Router();
170172
171173// Important: This route must be mounted as '/thumbnail'. It is used in the client code and saved to chat files.
172174router.get('/', jsonParser, async function (request, response) {
@@ -227,9 +229,3 @@ router.get('/', jsonParser, async function (request, response) {
227229 return response.sendStatus(500);
228230 }
229231});
230-
231-module.exports = {
232- invalidateThumbnail,
233- ensureThumbnailCache,
234- router,
235-};
src/endpoints/tokenizers.js+23 -29
@@ -1,15 +1,19 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
3+import { Buffer } from 'node:buffer';
4-const { SentencePieceProcessor } = require('@agnai/sentencepiece-js');
4+
55constimport tiktokenexpress =from require('tiktokenexpress');
66constimport { Tokenizersync as writeFileAtomicSync } =from require('@agnai/webwrite-tokenizersfile-atomic');
7-const { convertClaudePrompt } = require('../prompt-converters');
7+
88constimport { TEXTGEN_TYPESTokenizer } =from require('..@agnai/constantsweb-tokenizers');
99constimport { jsonParserSentencePieceProcessor } =from require('..@agnai/expresssentencepiece-commonjs');
10-const { setAdditionalHeaders } = require('../additional-headers');
10+import tiktoken from 'tiktoken';
11-const { getConfigValue, isValidUrl } = require('../util');
11+
12-const writeFileAtomicSync = require('write-file-atomic').sync;
12+import { convertClaudePrompt } from '../prompt-converters.js';
13+import { TEXTGEN_TYPES } from '../constants.js';
14+import { jsonParser } from '../express-common.js';
15+import { setAdditionalHeaders } from '../additional-headers.js';
16+import { getConfigValue, isValidUrl } from '../util.js';
1317
1418/**
1519 * @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler
@@ -23,7 +27,7 @@ const tokenizersCache = {};
2327/**
2428 * @type {string[]}
2529 */
2630export const TEXT_COMPLETION_MODELS = [
2731 'gpt-3.5-turbo-instruct',
2832 'gpt-3.5-turbo-instruct-0914',
2933 'text-davinci-003',
@@ -223,7 +227,7 @@ const commandTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyT
223227const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json', 'src/tokenizers/llama3.json');
224228const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json', 'src/tokenizers/llama3.json');
225229
226230export const sentencepieceTokenizers = [
227231 'llama',
228232 'nerdstash',
229233 'nerdstash_v2',
@@ -238,7 +242,7 @@ const sentencepieceTokenizers = [
238242 * @param {string} model Sentencepiece model name
239243 * @returns {SentencePieceTokenizer|null} Sentencepiece tokenizer
240244 */
241245export function getSentencepiceTokenizer(model) {
242246 if (model.includes('llama')) {
243247 return spp_llama;
244248 }
@@ -350,7 +354,7 @@ function getWebTokenizersChunks(tokenizer, ids) {
350354 * @param {string} requestModel Models to use for tokenization
351355 * @returns {string} Tokenizer model to use
352356 */
353357export function getTokenizerModel(requestModel) {
354358 if (requestModel.includes('o1-preview') || requestModel.includes('o1-mini')) {
355359 return 'gpt-4o';
356360 }
@@ -427,7 +431,7 @@ function getTokenizerModel(requestModel) {
427431 return 'gpt-3.5-turbo';
428432}
429433
430434export function getTiktokenTokenizer(model) {
431435 if (tokenizersCache[model]) {
432436 return tokenizersCache[model];
433437 }
@@ -444,7 +448,7 @@ function getTiktokenTokenizer(model) {
444448 * @param {object[]} messages Array of messages
445449 * @returns {number} Number of tokens
446450 */
447451export function countWebTokenizerTokens(tokenizer, messages) {
448452 // Should be fine if we use the old conversion method instead of the messages API one i think?
449453 const convertedPrompt = convertClaudePrompt(messages, false, '', false, false, '', false);
450454
@@ -636,7 +640,7 @@ function createWebTokenizerDecodingHandler(tokenizer) {
636640 };
637641}
638642
639643export const router = express.Router();
640644
641645router.post('/llama/encode', jsonParser, createSentencepieceEncodingHandler(spp_llama));
642646router.post('/nerdstash/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd));
@@ -1002,13 +1006,3 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
10021006 return response.send({ error: true });
10031007 }
10041008});
1005-
1006-module.exports = {
1007- TEXT_COMPLETION_MODELS,
1008- getTokenizerModel,
1009- getTiktokenTokenizer,
1010- countWebTokenizerTokens,
1011- getSentencepiceTokenizer,
1012- sentencepieceTokenizers,
1013- router,
1014-};
src/endpoints/translate.js+21 -54
@@ -1,29 +1,18 @@
1-const fetch = require('node-fetch').default;
1+import https from 'node:https';
2-const https = require('https');
2+
3-const express = require('express');
3+import fetch from 'node-fetch';
4-const iconv = require('iconv-lite');
4+import express from 'express';
5-const { readSecret, SECRET_KEYS } = require('./secrets');
5+import bingTranslateApi from 'bing-translate-api';
6-const { getConfigValue, uuidv4 } = require('../util');
6+import googleTranslateApi from 'google-translate-api-x';
7-const { jsonParser } = require('../express-common');
7+
8+import { readSecret, SECRET_KEYS } from './secrets.js';
9+import { getConfigValue, uuidv4 } from '../util.js';
10+import { jsonParser } from '../express-common.js';
811
912const DEEPLX_URL_DEFAULT = 'http://127.0.0.1:1188/translate';
1013const ONERING_URL_DEFAULT = 'http://127.0.0.1:4990/translate';
1114
12-/**
15+export const router = express.Router();
13- * Tries to decode a Node.js Buffer to a string using iconv-lite for UTF-8.
14- * @param {Buffer} buffer Node.js Buffer
15- * @returns {string} Decoded string
16- */
17-function decodeBuffer(buffer) {
18- try {
19- return iconv.decode(buffer, 'utf-8');
20- } catch (error) {
21- console.log('Failed to decode buffer:', error);
22- return buffer.toString('utf-8');
23- }
24-}
25-
26-const router = express.Router();
2716
2817router.post('/libre', jsonParser, async (request, response) => {
2918 const key = readSecret(request.user.directories, SECRET_KEYS.LIBRE);
@@ -70,6 +59,7 @@ router.post('/libre', jsonParser, async (request, response) => {
7059 return response.sendStatus(result.status);
7160 }
7261
62+ /** @type {any} */
7363 const json = await result.json();
7464 console.log('Translated text: ' + json.translatedText);
7565
@@ -82,7 +72,6 @@ router.post('/libre', jsonParser, async (request, response) => {
8272
8373router.post('/google', jsonParser, async (request, response) => {
8474 try {
85- const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser');
8675 const text = request.body.text;
8776 const lang = request.body.lang;
8877
@@ -92,31 +81,12 @@ router.post('/google', jsonParser, async (request, response) => {
9281
9382 console.log('Input text: ' + text);
9483
9584 const urlresult = generateRequestUrlawait googleTranslateApi(text, { to: lang, forceBatch: false });
85+ const translatedText = Array.isArray(result) ? result.map(x => x.text).join('') : result.text;
9686
97- https.get(url, (resp) => {
87+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
98- const data = [];
88+ console.log('Translated text: ' + translatedText);
99-
89+ return response.send(translatedText);
100- resp.on('data', (chunk) => {
101- data.push(chunk);
102- });
103-
104- resp.on('end', () => {
105- try {
106- const decodedData = decodeBuffer(Buffer.concat(data));
107- const result = normaliseResponse(JSON.parse(decodedData));
108- console.log('Translated text: ' + result.text);
109- response.setHeader('Content-Type', 'text/plain; charset=utf-8');
110- return response.send(result.text);
111- } catch (error) {
112- console.log('Translation error', error);
113- return response.sendStatus(500);
114- }
115- });
116- }).on('error', (err) => {
117- console.log('Translation error: ' + err.message);
118- return response.sendStatus(500);
119- });
12090 } catch (error) {
12191 console.log('Translation error', error);
12292 return response.sendStatus(500);
@@ -151,7 +121,6 @@ router.post('/yandex', jsonParser, async (request, response) => {
151121 headers: {
152122 'Content-Type': 'application/x-www-form-urlencoded',
153123 },
154- timeout: 0,
155124 });
156125
157126 if (!result.ok) {
@@ -160,6 +129,7 @@ router.post('/yandex', jsonParser, async (request, response) => {
160129 return response.sendStatus(500);
161130 }
162131
132+ /** @type {any} */
163133 const json = await result.json();
164134 const translated = json.text.join();
165135 console.log('Translated text: ' + translated);
@@ -257,7 +227,6 @@ router.post('/deepl', jsonParser, async (request, response) => {
257227 'Authorization': `DeepL-Auth-Key ${key}`,
258228 'Content-Type': 'application/x-www-form-urlencoded',
259229 },
260- timeout: 0,
261230 });
262231
263232 if (!result.ok) {
@@ -266,6 +235,7 @@ router.post('/deepl', jsonParser, async (request, response) => {
266235 return response.sendStatus(result.status);
267236 }
268237
238+ /** @type {any} */
269239 const json = await result.json();
270240 console.log('Translated text: ' + json.translations[0].text);
271241
@@ -310,7 +280,6 @@ router.post('/onering', jsonParser, async (request, response) => {
310280
311281 const result = await fetch(fetchUrl, {
312282 method: 'GET',
313- timeout: 0,
314283 });
315284
316285 if (!result.ok) {
@@ -319,6 +288,7 @@ router.post('/onering', jsonParser, async (request, response) => {
319288 return response.sendStatus(result.status);
320289 }
321290
291+ /** @type {any} */
322292 const data = await result.json();
323293 console.log('Translated text: ' + data.result);
324294
@@ -366,7 +336,6 @@ router.post('/deeplx', jsonParser, async (request, response) => {
366336 'Accept': 'application/json',
367337 'Content-Type': 'application/json',
368338 },
369- timeout: 0,
370339 });
371340
372341 if (!result.ok) {
@@ -375,6 +344,7 @@ router.post('/deeplx', jsonParser, async (request, response) => {
375344 return response.sendStatus(result.status);
376345 }
377346
347+ /** @type {any} */
378348 const json = await result.json();
379349 console.log('Translated text: ' + json.data);
380350
@@ -386,7 +356,6 @@ router.post('/deeplx', jsonParser, async (request, response) => {
386356});
387357
388358router.post('/bing', jsonParser, async (request, response) => {
389- const bingTranslateApi = require('bing-translate-api');
390359 const text = request.body.text;
391360 let lang = request.body.lang;
392361
@@ -408,5 +377,3 @@ router.post('/bing', jsonParser, async (request, response) => {
408377 return response.sendStatus(500);
409378 });
410379});
411-
412-module.exports = { router };
src/endpoints/users-admin.js+17 -20
@@ -1,10 +1,11 @@
1-const fsPromises = require('fs').promises;
1+import { promises as fsPromises } from 'node:fs';
2-const storage = require('node-persist');
2+
3-const express = require('express');
3+import storage from 'node-persist';
44constimport lodashexpress =from require('lodashexpress');
5-const { jsonParser } = require('../express-common');
5+import lodash from 'lodash';
66constimport { checkForNewContentjsonParser } =from require('../contentexpress-managercommon.js');
7-const {
7+import { checkForNewContent } from './content-manager.js';
8+import {
89 KEY_PREFIX,
910 toKey,
1011 requireAdminMiddleware,
@@ -14,17 +15,17 @@ const {
1415 getPasswordHash,
1516 getUserDirectories,
1617 ensurePublicDirectoriesExist,
1718} =from require('../users.js');
1819constimport { DEFAULT_USER } =from require('../constants.js');
1920
2021export const router = express.Router();
2122
2223router.post('/get', requireAdminMiddleware, jsonParser, async (_request, response) => {
2324 try {
2425 /** @type {import('../users.js').User[]} */
2526 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
2627
2728 /** @type {Promise<import('../users.js').UserViewModel>[]} */
2829 const viewModelPromises = users
2930 .map(user => new Promise(resolve => {
3031 getUserAvatar(user.handle).then(avatar =>
@@ -61,7 +62,7 @@ router.post('/disable', requireAdminMiddleware, jsonParser, async (request, resp
6162 return response.status(400).json({ error: 'Cannot disable yourself' });
6263 }
6364
6465 /** @type {import('../users.js').User} */
6566 const user = await storage.getItem(toKey(request.body.handle));
6667
6768 if (!user) {
@@ -85,7 +86,7 @@ router.post('/enable', requireAdminMiddleware, jsonParser, async (request, respo
8586 return response.status(400).json({ error: 'Missing required fields' });
8687 }
8788
8889 /** @type {import('../users.js').User} */
8990 const user = await storage.getItem(toKey(request.body.handle));
9091
9192 if (!user) {
@@ -109,7 +110,7 @@ router.post('/promote', requireAdminMiddleware, jsonParser, async (request, resp
109110 return response.status(400).json({ error: 'Missing required fields' });
110111 }
111112
112113 /** @type {import('../users.js').User} */
113114 const user = await storage.getItem(toKey(request.body.handle));
114115
115116 if (!user) {
@@ -138,7 +139,7 @@ router.post('/demote', requireAdminMiddleware, jsonParser, async (request, respo
138139 return response.status(400).json({ error: 'Cannot demote yourself' });
139140 }
140141
141142 /** @type {import('../users.js').User} */
142143 const user = await storage.getItem(toKey(request.body.handle));
143144
144145 if (!user) {
@@ -249,7 +250,3 @@ router.post('/slugify', requireAdminMiddleware, jsonParser, async (request, resp
249250 return response.sendStatus(500);
250251 }
251252});
252-
253-module.exports = {
254- router,
255-};
src/endpoints/users-private.js+17 -20
@@ -1,18 +1,19 @@
11constimport path =from require('node:path');
2-const fsPromises = require('fs').promises;
2+import { promises as fsPromises } from 'node:fs';
33constimport storagecrypto =from require('node-persist:crypto');
4-const express = require('express');
4+
5-const crypto = require('crypto');
5+import storage from 'node-persist';
6-const { jsonParser } = require('../express-common');
6+import express from 'express';
7-const { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } = require('../users');
7+
88constimport { SETTINGS_FILEjsonParser } =from require('../constantsexpress-common.js');
9-const contentManager = require('./content-manager');
9+import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js';
1010constimport { color, CacheSETTINGS_FILE } =from require('../utilconstants.js');
1111constimport { checkForNewContent, CONTENT_TYPES } =from require('./content-manager.js');
12+import { color, Cache } from '../util.js';
1213
1314const RESET_CACHE = new Cache(5 * 60 * 1000);
1415
1516export const router = express.Router();
1617
1718router.post('/logout', async (request, response) => {
1819 try {
@@ -70,7 +71,7 @@ router.post('/change-avatar', jsonParser, async (request, response) => {
7071 return response.status(400).json({ error: 'Invalid data URL' });
7172 }
7273
7374 /** @type {import('../users.js').User} */
7475 const user = await storage.getItem(toKey(request.body.handle));
7576
7677 if (!user) {
@@ -99,7 +100,7 @@ router.post('/change-password', jsonParser, async (request, response) => {
99100 return response.status(403).json({ error: 'Unauthorized' });
100101 }
101102
102103 /** @type {import('../users.js').User} */
103104 const user = await storage.getItem(toKey(request.body.handle));
104105
105106 if (!user) {
@@ -166,7 +167,7 @@ router.post('/reset-settings', jsonParser, async (request, response) => {
166167
167168 const pathToFile = path.join(request.user.directories.root, SETTINGS_FILE);
168169 await fsPromises.rm(pathToFile, { force: true });
169170 await contentManager.checkForNewContent([request.user.directories], [contentManager.CONTENT_TYPES.SETTINGS]);
170171
171172 return response.sendStatus(204);
172173 } catch (error) {
@@ -187,7 +188,7 @@ router.post('/change-name', jsonParser, async (request, response) => {
187188 return response.status(403).json({ error: 'Unauthorized' });
188189 }
189190
190191 /** @type {import('../users.js').User} */
191192 const user = await storage.getItem(toKey(request.body.handle));
192193
193194 if (!user) {
@@ -251,7 +252,3 @@ router.post('/reset-step2', jsonParser, async (request, response) => {
251252 return response.sendStatus(500);
252253 }
253254});
254-
255-module.exports = {
256- router,
257-};
src/endpoints/users-public.js+14 -17
@@ -1,15 +1,16 @@
11constimport crypto =from require('node:crypto');
2-const storage = require('node-persist');
2+
3-const express = require('express');
3+import storage from 'node-persist';
4-const { RateLimiterMemory, RateLimiterRes } = require('rate-limiter-flexible');
4+import express from 'express';
55constimport { jsonParserRateLimiterMemory, getIpFromRequestRateLimiterRes } =from require('../expressrate-commonlimiter-flexible');
66constimport { color, CachejsonParser, getConfigValuegetIpFromRequest } =from require('../utilexpress-common.js');
77constimport { KEY_PREFIX, getUserAvatar, toKeycolor, getPasswordHashCache, getPasswordSaltgetConfigValue } =from require('../usersutil.js');
8+import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
89
910const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
1011const MFA_CACHE = new Cache(5 * 60 * 1000);
1112
1213export const router = express.Router();
1314const loginLimiter = new RateLimiterMemory({
1415 points: 5,
1516 duration: 60,
@@ -25,10 +26,10 @@ router.post('/list', async (_request, response) => {
2526 return response.sendStatus(204);
2627 }
2728
2829 /** @type {import('../users.js').User[]} */
2930 const users = await storage.values(x => x.key.startsWith(KEY_PREFIX));
3031
3132 /** @type {Promise<import('../users.js').UserViewModel>[]} */
3233 const viewModelPromises = users
3334 .filter(x => x.enabled)
3435 .map(user => new Promise(async (resolve) => {
@@ -62,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
6263 const ip = getIpFromRequest(request);
6364 await loginLimiter.consume(ip);
6465
6566 /** @type {import('../users.js').User} */
6667 const user = await storage.getItem(toKey(request.body.handle));
6768
6869 if (!user) {
@@ -110,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
110111 const ip = getIpFromRequest(request);
111112 await recoverLimiter.consume(ip);
112113
113114 /** @type {import('../users.js').User} */
114115 const user = await storage.getItem(toKey(request.body.handle));
115116
116117 if (!user) {
@@ -147,7 +148,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
147148 return response.status(400).json({ error: 'Missing required fields' });
148149 }
149150
150151 /** @type {import('../users.js').User} */
151152 const user = await storage.getItem(toKey(request.body.handle));
152153 const ip = getIpFromRequest(request);
153154
@@ -193,7 +194,3 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
193194 return response.sendStatus(500);
194195 }
195196});
196-
197-module.exports = {
198- router,
199-};
src/endpoints/vectors.js+46 -36
@@ -1,10 +1,22 @@
1-const vectra = require('vectra');
1+import path from 'node:path';
2-const path = require('path');
2+import fs from 'node:fs';
3-const fs = require('fs');
3+
44constimport expressvectra =from require('expressvectra');
5-const sanitize = require('sanitize-filename');
5+import express from 'express';
6-const { jsonParser } = require('../express-common');
6+import sanitize from 'sanitize-filename';
7-const { getConfigValue } = require('../util');
7+
8+import { jsonParser } from '../express-common.js';
9+import { getConfigValue } from '../util.js';
10+
11+import { getNomicAIBatchVector, getNomicAIVector } from '../vectors/nomicai-vectors.js';
12+import { getOpenAIVector, getOpenAIBatchVector } from '../vectors/openai-vectors.js';
13+import { getTransformersVector, getTransformersBatchVector } from '../vectors/embedding.js';
14+import { getExtrasVector, getExtrasBatchVector } from '../vectors/extras-vectors.js';
15+import { getMakerSuiteVector, getMakerSuiteBatchVector } from '../vectors/makersuite-vectors.js';
16+import { getCohereVector, getCohereBatchVector } from '../vectors/cohere-vectors.js';
17+import { getLlamaCppVector, getLlamaCppBatchVector } from '../vectors/llamacpp-vectors.js';
18+import { getVllmVector, getVllmBatchVector } from '../vectors/vllm-vectors.js';
19+import { getOllamaVector, getOllamaBatchVector } from '../vectors/ollama-vectors.js';
820
921// Don't forget to add new sources to the SOURCES array
1022const SOURCES = [
@@ -27,31 +39,31 @@ const SOURCES = [
2739 * @param {Object} sourceSettings - Settings for the source, if it needs any
2840 * @param {string} text - The text to get the vector for
2941 * @param {boolean} isQuery - If the text is a query for embedding search
3042 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
3143 * @returns {Promise<number[]>} - The vector for the text
3244 */
3345async function getVector(source, sourceSettings, text, isQuery, directories) {
3446 switch (source) {
3547 case 'nomicai':
3648 return require('../vectors/nomicai-vectors').getNomicAIVector(text, source, directories);
3749 case 'togetherai':
3850 case 'mistral':
3951 case 'openai':
4052 return require('../vectors/openai-vectors').getOpenAIVector(text, source, directories, sourceSettings.model);
4153 case 'transformers':
4254 return require('../vectors/embedding').getTransformersVector(text);
4355 case 'extras':
4456 return require('../vectors/extras-vectors').getExtrasVector(text, sourceSettings.extrasUrl, sourceSettings.extrasKey);
4557 case 'palm':
4658 return require('../vectors/makersuite-vectors').getMakerSuiteVector(text, directories);
4759 case 'cohere':
4860 return require('../vectors/cohere-vectors').getCohereVector(text, isQuery, directories, sourceSettings.model);
4961 case 'llamacpp':
5062 return require('../vectors/llamacpp-vectors').getLlamaCppVector(text, sourceSettings.apiUrl, directories);
5163 case 'vllm':
5264 return require('../vectors/vllm-vectors').getVllmVector(text, sourceSettings.apiUrl, sourceSettings.model, directories);
5365 case 'ollama':
5466 return require('../vectors/ollama-vectors').getOllamaVector(text, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories);
5567 }
5668
5769 throw new Error(`Unknown vector source ${source}`);
@@ -63,7 +75,7 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
6375 * @param {Object} sourceSettings - Settings for the source, if it needs any
6476 * @param {string[]} texts - The array of texts to get the vector for
6577 * @param {boolean} isQuery - If the text is a query for embedding search
6678 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
6779 * @returns {Promise<number[][]>} - The array of vectors for the texts
6880 */
6981async function getBatchVector(source, sourceSettings, texts, isQuery, directories) {
@@ -74,33 +86,33 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
7486 for (let batch of batches) {
7587 switch (source) {
7688 case 'nomicai':
7789 results.push(...await require('../vectors/nomicai-vectors').getNomicAIBatchVector(batch, source, directories));
7890 break;
7991 case 'togetherai':
8092 case 'mistral':
8193 case 'openai':
8294 results.push(...await require('../vectors/openai-vectors').getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
8395 break;
8496 case 'transformers':
8597 results.push(...await require('../vectors/embedding').getTransformersBatchVector(batch));
8698 break;
8799 case 'extras':
88100 results.push(...await require('../vectors/extras-vectors').getExtrasBatchVector(batch, sourceSettings.extrasUrl, sourceSettings.extrasKey));
89101 break;
90102 case 'palm':
91103 results.push(...await require('../vectors/makersuite-vectors').getMakerSuiteBatchVector(batch, directories));
92104 break;
93105 case 'cohere':
94106 results.push(...await require('../vectors/cohere-vectors').getCohereBatchVector(batch, isQuery, directories, sourceSettings.model));
95107 break;
96108 case 'llamacpp':
97109 results.push(...await require('../vectors/llamacpp-vectors').getLlamaCppBatchVector(batch, sourceSettings.apiUrl, directories));
98110 break;
99111 case 'vllm':
100112 results.push(...await require('../vectors/vllm-vectors').getVllmBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, directories));
101113 break;
102114 case 'ollama':
103115 results.push(...await require('../vectors/ollama-vectors').getOllamaBatchVector(batch, sourceSettings.apiUrl, sourceSettings.model, sourceSettings.keep, directories));
104116 break;
105117 default:
106118 throw new Error(`Unknown vector source ${source}`);
@@ -183,7 +195,7 @@ function getModelScope(sourceSettings) {
183195
184196/**
185197 * Gets the index for the vector collection
186198 * @param {import('../users.js').UserDirectoryList} directories - User directories
187199 * @param {string} collectionId - The collection ID
188200 * @param {string} source - The source of the vector
189201 * @param {object} sourceSettings - The model for the source
@@ -203,7 +215,7 @@ async function getIndex(directories, collectionId, source, sourceSettings) {
203215
204216/**
205217 * Inserts items into the vector collection
206218 * @param {import('../users.js').UserDirectoryList} directories - User directories
207219 * @param {string} collectionId - The collection ID
208220 * @param {string} source - The source of the vector
209221 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -227,7 +239,7 @@ async function insertVectorItems(directories, collectionId, source, sourceSettin
227239
228240/**
229241 * Gets the hashes of the items in the vector collection
230242 * @param {import('../users.js').UserDirectoryList} directories - User directories
231243 * @param {string} collectionId - The collection ID
232244 * @param {string} source - The source of the vector
233245 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -244,7 +256,7 @@ async function getSavedHashes(directories, collectionId, source, sourceSettings)
244256
245257/**
246258 * Deletes items from the vector collection by hash
247259 * @param {import('../users.js').UserDirectoryList} directories - User directories
248260 * @param {string} collectionId - The collection ID
249261 * @param {string} source - The source of the vector
250262 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -265,7 +277,7 @@ async function deleteVectorItems(directories, collectionId, source, sourceSettin
265277
266278/**
267279 * Gets the hashes of the items in the vector collection that match the search text
268280 * @param {import('../users.js').UserDirectoryList} directories - User directories
269281 * @param {string} collectionId - The collection ID
270282 * @param {string} source - The source of the vector
271283 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -286,7 +298,7 @@ async function queryCollection(directories, collectionId, source, sourceSettings
286298
287299/**
288300 * Queries multiple collections for the given search queries. Returns the overall top K results.
289301 * @param {import('../users.js').UserDirectoryList} directories - User directories
290302 * @param {string[]} collectionIds - The collection IDs to query
291303 * @param {string} source - The source of the vector
292304 * @param {Object} sourceSettings - Settings for the source, if it needs any
@@ -359,7 +371,7 @@ async function regenerateCorruptedIndexErrorHandler(req, res, error) {
359371 return res.sendStatus(500);
360372}
361373
362374export const router = express.Router();
363375
364376router.post('/query', jsonParser, async (req, res) => {
365377 try {
@@ -495,5 +507,3 @@ router.post('/purge', jsonParser, async (req, res) => {
495507 return res.sendStatus(500);
496508 }
497509});
498-
499-module.exports = { router };
src/endpoints/worldinfo.js+10 -11
@@ -1,19 +1,20 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
3-const express = require('express');
4-const sanitize = require('sanitize-filename');
5-const writeFileAtomicSync = require('write-file-atomic').sync;
63
7-const { jsonParser, urlencodedParser } = require('../express-common');
4+import express from 'express';
5+import sanitize from 'sanitize-filename';
6+import { sync as writeFileAtomicSync } from 'write-file-atomic';
7+
8+import { jsonParser, urlencodedParser } from '../express-common.js';
89
910/**
1011 * Reads a World Info file and returns its contents
1112 * @param {import('../users.js').UserDirectoryList} directories User directories
1213 * @param {string} worldInfoName Name of the World Info file
1314 * @param {boolean} allowDummy If true, returns an empty object if the file doesn't exist
1415 * @returns {object} World Info file contents
1516 */
1617export function readWorldInfoFile(directories, worldInfoName, allowDummy) {
1718 const dummyObject = allowDummy ? { entries: {} } : null;
1819
1920 if (!worldInfoName) {
@@ -33,7 +34,7 @@ function readWorldInfoFile(directories, worldInfoName, allowDummy) {
3334 return worldInfo;
3435}
3536
3637export const router = express.Router();
3738
3839router.post('/get', jsonParser, (request, response) => {
3940 if (!request.body?.name) {
@@ -122,5 +123,3 @@ router.post('/edit', jsonParser, (request, response) => {
122123
123124 return response.send({ ok: true });
124125});
125-
126-module.exports = { router, readWorldInfoFile };
src/express-common.js+11 -12
@@ -1,28 +1,27 @@
11constimport express =from require('express');
22constimport ipaddr =from require('ipaddr.js');
33
44// Instantiate parser middleware here with application-level size limits
55export const jsonParser = express.json({ limit: '200mb' });
66export const urlencodedParser = express.urlencoded({ extended: true, limit: '200mb' });
77
88/**
99 * Gets the IP address of the client from the request object.
1010 * @param {import('express').Request)} req Request object
1111 * @returns {string} IP address of the client
1212 */
1313export function getIpFromRequest(req) {
1414 let clientIp = req.connectionsocket.remoteAddress;
15+ if (!clientIp) {
16+ return 'unknown';
17+ }
1518 let ip = ipaddr.parse(clientIp);
1619 // Check if the IP address is IPv4-mapped IPv6 address
1720 if (ip.kind() === 'ipv6' && ip instanceof ipaddr.IPv6 && ip.isIPv4MappedAddress()) {
1821 const ipv4 = ip.toIPv4Address().toString();
1922 clientIp = ipv4;
2023 } else {
2124 clientIp = ip.toString();
22- clientIp = clientIp.toString();
2325 }
2426 return clientIp;
2527}
26-
27-
28-module.exports = { jsonParser, urlencodedParser, getIpFromRequest };
src/middleware/basicAuth.js+5 -4
@@ -2,9 +2,10 @@
22 * When applied, this middleware will ensure the request contains the required header for basic authentication and only
33 * allow access to the endpoint after successful authentication.
44 */
5-const { getAllUserHandles, toKey, getPasswordHash } = require('../users.js');
5+import { Buffer } from 'node:buffer';
6-const { getConfig, getConfigValue } = require('../util.js');
6+import storage from 'node-persist';
7-const storage = require('node-persist');
7+import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
8+import { getConfig, getConfigValue } from '../util.js';
89
910const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
1011const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
@@ -49,4 +50,4 @@ const basicAuthMiddleware = async function (request, response, callback) {
4950 return unauthorizedResponse(response);
5051};
5152
5253module.exportsexport =default basicAuthMiddleware;
src/middleware/multerMonkeyPatch.js+3 -3
@@ -1,3 +1,5 @@
1+import { Buffer } from 'node:buffer';
2+
13/**
24 * Decodes a file name from Latin1 to UTF-8.
35 * @param {string} str Input string
@@ -14,7 +16,7 @@ function decodeFileName(str) {
1416 * @param {import('express').Response} _res Response
1517 * @param {import('express').NextFunction} next Next middleware
1618 */
1719export default function multerMonkeyPatch(req, _res, next) {
1820 try {
1921 if (req.file) {
2022 req.file.originalname = decodeFileName(req.file.originalname);
@@ -26,5 +28,3 @@ function multerMonkeyPatch(req, _res, next) {
2628 next();
2729 }
2830}
29-
30-module.exports = multerMonkeyPatch;
src/middleware/whitelist.js+7 -8
@@ -1,9 +1,10 @@
11constimport path =from require('node:path');
22constimport fs =from require('node:fs');
3-const ipMatching = require('ip-matching');
3+import process from 'node:process';
4+import ipMatching from 'ip-matching';
45
56constimport { getIpFromRequest } =from require('../express-common.js');
67constimport { color, getConfigValue } =from require('../util.js');
78
89const whitelistPath = path.join(process.cwd(), './whitelist.txt');
910const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
@@ -50,7 +51,7 @@ function getForwardedIp(req) {
5051 * @param {boolean} listen If listen mode is enabled via config or command line
5152 * @returns {import('express').RequestHandler} The middleware function
5253 */
5354export default function whitelistMiddleware(whitelistMode, listen) {
5455 return function (req, res, next) {
5556 const clientIp = getIpFromRequest(req);
5657 const forwardedIp = getForwardedIp(req);
@@ -82,5 +83,3 @@ function whitelistMiddleware(whitelistMode, listen) {
8283 next();
8384 };
8485}
85-
86-module.exports = whitelistMiddleware;
src/plugin-loader.js+17 -24
@@ -1,8 +1,9 @@
11constimport fs =from require('node:fs');
22constimport path =from require('node:path');
33constimport url =from require('node:url');
4-const express = require('express');
4+
5-const { getConfigValue } = require('./util');
5+import express from 'express';
6+import { getConfigValue } from './util.js';
67const enableServerPlugins = getConfigValue('enableServerPlugins', false);
78
89/**
@@ -16,7 +17,7 @@ const loadedPlugins = new Map();
1617 * @param {string} file Path to file
1718 * @returns {boolean} True if file is a CommonJS module
1819 */
1920const isCommonJS = (file) => path.extname(file) === '.js' || path.extname(file) === '.cjs';
2021
2122/**
2223 * Determine if a file is an ECMAScript module.
@@ -32,9 +33,9 @@ const isESModule = (file) => path.extname(file) === '.mjs';
3233 * @returns {Promise<Function>} Promise that resolves when all plugins are loaded. Resolves to a "cleanup" function to
3334 * be called before the server shuts down.
3435 */
3536export async function loadPlugins(app, pluginsPath) {
3637 const exitHooks = [];
3738 const emptyFn = () => { };
3839
3940 // Server plugins are disabled.
4041 if (!enableServerPlugins) {
@@ -89,19 +90,15 @@ async function loadFromDirectory(app, pluginDirectoryPath, exitHooks) {
8990 }
9091 }
9192
9293 // Plugin is a CommonJS module file.
9394 const cjsFilePathfileTypes = path['index.join(pluginDirectoryPathjs', 'index.jscjs'), 'index.mjs'];
94- if (fs.existsSync(cjsFilePath)) {
95- if (await loadFromFile(app, cjsFilePath, exitHooks)) {
96- return;
97- }
98- }
9995
100- // Plugin is an ECMAScript module.
96+ for (const fileType of fileTypes) {
10197 const esmFilePathfilePath = path.join(pluginDirectoryPath, 'index.mjs'fileType);
10298 if (fs.existsSync(esmFilePathfilePath)) {
10399 if (await loadFromFile(app, esmFilePathfilePath, exitHooks)) {
104100 return;
101+ }
105102 }
106103 }
107104}
@@ -217,7 +214,3 @@ async function initPlugin(app, plugin, exitHooks) {
217214
218215 return true;
219216}
220-
221-module.exports = {
222- loadPlugins,
223-};
src/polyfill.js+0 -10
@@ -1,10 +0,0 @@
1-if (!Array.prototype.findLastIndex) {
2- Array.prototype.findLastIndex = function (callback, thisArg) {
3- for (let i = this.length - 1; i >= 0; i--) {
4- if (callback.call(thisArg, this[i], i, this)) return i;
5- }
6- return -1;
7- };
8-}
9-
10-module.exports = {};
src/prompt-converters.js+10 -22
@@ -1,6 +1,5 @@
1-require('./polyfill.js');
1+import crypto from 'node:crypto';
22constimport { getConfigValue } =from require('./util.js');
3-const crypto = require('crypto');
43
54const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
65
@@ -17,7 +16,7 @@ const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get start
1716 * @returns {string} Prompt for Claude
1817 * @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).
1918 */
2019export function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg, excludePrefixes) {
2120
2221 //Prepare messages for claude.
2322 //When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
@@ -96,7 +95,7 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
9695 * @param {string} charName Character name
9796 * @param {string} userName User name
9897 */
9998export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, humanMsgFix, charName = '', userName = '') {
10099 let systemPrompt = [];
101100 if (useSysPrompt) {
102101 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@@ -279,7 +278,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools,
279278 * @param {string} userName User name
280279 * @returns {{chatHistory: object[]}} Prompt for Cohere
281280 */
282281export function convertCohereMessages(messages, charName = '', userName = '') {
283282 if (messages.length === 0) {
284283 messages.unshift({
285284 role: 'user',
@@ -333,7 +332,7 @@ function convertCohereMessages(messages, charName = '', userName = '') {
333332 * @param {string} userName User name
334333 * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models
335334 */
336335export function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = '') {
337336 // This is a 1x1 transparent PNG
338337 const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
339338
@@ -456,7 +455,7 @@ function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '
456455 * @param {string} charName Character name
457456 * @param {string} userName User name
458457 */
459458export function convertAI21Messages(messages, charName = '', userName = '') {
460459 if (!Array.isArray(messages)) {
461460 return [];
462461 }
@@ -528,7 +527,7 @@ function convertAI21Messages(messages, charName = '', userName = '') {
528527 * @param {string} charName Character name
529528 * @param {string} userName User name
530529 */
531530export function convertMistralMessages(messages, charName = '', userName = '') {
532531 if (!Array.isArray(messages)) {
533532 return [];
534533 }
@@ -612,7 +611,7 @@ function convertMistralMessages(messages, charName = '', userName = '') {
612611 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
613612 * @returns {any[]} Merged messages
614613 */
615614export function mergeMessages(messages, charName, userName, strict) {
616615 let mergedMessages = [];
617616
618617 // Remove names from the messages
@@ -686,7 +685,7 @@ function mergeMessages(messages, charName, userName, strict) {
686685 * @param {object[]} messages Array of messages
687686 * @returns {string} Prompt for Text Completion API
688687 */
689688export function convertTextCompletionPrompt(messages) {
690689 if (typeof messages === 'string') {
691690 return messages;
692691 }
@@ -705,14 +704,3 @@ function convertTextCompletionPrompt(messages) {
705704 });
706705 return messageStrings.join('\n') + '\nassistant:';
707706}
708-
709-module.exports = {
710- convertClaudePrompt,
711- convertClaudeMessages,
712- convertGooglePrompt,
713- convertTextCompletionPrompt,
714- convertCohereMessages,
715- convertMistralMessages,
716- convertAI21Messages,
717- mergeMessages,
718-};
src/request-proxy.js+6 -10
@@ -1,7 +1,8 @@
11constimport httpprocess =from require('node:httpprocess');
22constimport httpshttp =from require('node:httpshttp');
3-
3+import https from 'node:https';
44constimport { isValidUrl, colorProxyAgent } =from require('./util.jsproxy-agent');
5+import { isValidUrl, color } from './util.js';
56
67const LOG_HEADER = '[Request Proxy]';
78
@@ -13,10 +14,8 @@ const LOG_HEADER = '[Request Proxy]';
1314 * @property {string} url Proxy URL.
1415 * @property {string[]} bypass List of URLs to bypass proxy.
1516 */
1617export default function initRequestProxy({ enabled, url, bypass }) {
1718 try {
18- const { ProxyAgent } = require('proxy-agent');
19-
2019 // No proxy is enabled, so return
2120 if (!enabled) {
2221 return;
@@ -36,7 +35,6 @@ function initRequestProxy({ enabled, url, bypass }) {
3635 // Reference: https://github.com/Rob--W/proxy-from-env
3736 process.env.all_proxy = url;
3837
39-
4038 if (Array.isArray(bypass) && bypass.length > 0) {
4139 process.env.no_proxy = bypass.join(',');
4240 }
@@ -52,5 +50,3 @@ function initRequestProxy({ enabled, url, bypass }) {
5250 console.error(color.red(LOG_HEADER), 'Failed to initialize request proxy:', error);
5351 }
5452}
55-
56-module.exports = initRequestProxy;
src/transformers.mjs+11 -7
@@ -1,7 +1,10 @@
1-import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';
1+import path from 'node:path';
2+import fs from 'node:fs';
3+import process from 'node:process';
4+import { Buffer } from 'node:buffer';
5+
6+import { pipeline, env, RawImage } from 'sillytavern-transformers';
27import { getConfigValue } from './util.js';
3-import path from 'path';
4-import fs from 'fs';
58
69configureTransformers();
710
@@ -50,7 +53,7 @@ const tasks = {
5053 * @param {string} image Base64-encoded image
5154 * @returns {Promise<RawImage|null>} Object representing the image
5255 */
5356export async function getRawImage(image) {
5457 try {
5558 const buffer = Buffer.from(image, 'base64');
5659 const byteArray = new Uint8Array(buffer);
@@ -114,9 +117,9 @@ async function migrateCacheToDataDir() {
114117 * Gets the transformers.js pipeline for a given task.
115118 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for
116119 * @param {string} forceModel The model to use for the pipeline, if any
117120 * @returns {Promise<import('sillytavern-transformers').Pipeline>} Pipeline forThe thetransformers.js taskpipeline
118121 */
119122export async function getPipeline(task, forceModel = '') {
120123 await migrateCacheToDataDir();
121124
122125 if (tasks[task].pipeline) {
@@ -134,10 +137,11 @@ async function getPipeline(task, forceModel = '') {
134137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
135138 tasks[task].pipeline = instance;
136139 tasks[task].currentModel = model;
140+ // @ts-ignore
137141 return instance;
138142}
139143
140144export default {
141- getPipeline,
142145 getRawImage,
146+ getPipeline,
143147};
src/users.js+44 -70
@@ -1,22 +1,25 @@
11// Native Node Modules
22constimport path =from require('node:path');
33constimport fs =from require('node:fs');
44constimport crypto =from require('node:crypto');
55constimport os =from require('node:os');
6+import process from 'node:process';
7+import { Buffer } from 'node:buffer';
68
79// Express and other dependencies
810constimport storage =from require('node-persist');
911constimport express =from require('express');
1012constimport mime =from require('mime-types');
1113constimport archiver =from require('archiver');
12-const writeFileAtomicSync = require('write-file-atomic').sync;
14+import _ from 'lodash';
13-const _ = require('lodash');
15+import { sync as writeFileAtomicSync } from 'write-file-atomic';
1416
1517constimport { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } =from require('./constants.js');
1618constimport { getConfigValue, color, delay, setConfigValue, generateTimestamp } =from require('./util.js');
1719constimport { readSecret, writeSecret } =from require('./endpoints/secrets.js');
18-
20+import { getContentOfType } from './endpoints/content-manager.js';
19-const KEY_PREFIX = 'user:';
21+
22+export const KEY_PREFIX = 'user:';
2023const AVATAR_PREFIX = 'avatar:';
2124const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
2225const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false);
@@ -92,9 +95,9 @@ const STORAGE_KEYS = {
9295
9396/**
9497 * Ensures that the content directories exist.
9598 * @returns {Promise<import('./users.js').UserDirectoryList[]>} - The list of user directories
9699 */
97100export async function ensurePublicDirectoriesExist() {
98101 for (const dir of Object.values(PUBLIC_DIRECTORIES)) {
99102 if (!fs.existsSync(dir)) {
100103 fs.mkdirSync(dir, { recursive: true });
@@ -115,9 +118,9 @@ async function ensurePublicDirectoriesExist() {
115118
116119/**
117120 * Gets a list of all user directories.
118121 * @returns {Promise<import('./users.js').UserDirectoryList[]>} - The list of user directories
119122 */
120123export async function getUserDirectoriesList() {
121124 const userHandles = await getAllUserHandles();
122125 const directoriesList = userHandles.map(handle => getUserDirectories(handle));
123126 return directoriesList;
@@ -126,7 +129,7 @@ async function getUserDirectoriesList() {
126129/**
127130 * Perform migration from the old user data format to the new one.
128131 */
129132export async function migrateUserData() {
130133 const publicDirectory = path.join(process.cwd(), 'public');
131134
132135 // No need to migrate if the characters directory doesn't exists
@@ -328,14 +331,13 @@ async function migrateUserData() {
328331 console.log(color.green('Migration completed!'));
329332}
330333
331334export async function migrateSystemPrompts() {
332335 /**
333336 * Gets the default system prompts.
334337 * @returns {Promise<any[]>} - The list of default system prompts
335338 */
336339 async function getDefaultSystemPrompts() {
337340 try {
338- const { getContentOfType } = await import('./endpoints/content-manager.js');
339341 return getContentOfType('sysprompt', 'json');
340342 } catch {
341343 return [];
@@ -391,7 +393,7 @@ async function migrateSystemPrompts() {
391393 * @param {string} handle User handle
392394 * @returns {string} The key for the user storage
393395 */
394396export function toKey(handle) {
395397 return `${KEY_PREFIX}${handle}`;
396398}
397399
@@ -400,7 +402,7 @@ function toKey(handle) {
400402 * @param {string} handle User handle
401403 * @returns {string} The key for the avatar storage
402404 */
403405export function toAvatarKey(handle) {
404406 return `${AVATAR_PREFIX}${handle}`;
405407}
406408
@@ -409,7 +411,7 @@ function toAvatarKey(handle) {
409411 * @param {string} dataRoot The root directory for user data
410412 * @returns {Promise<void>}
411413 */
412414export async function initUserStorage(dataRoot) {
413415 global.DATA_ROOT = dataRoot;
414416 console.log('Using data root:', color.green(global.DATA_ROOT));
415417 console.log();
@@ -430,7 +432,7 @@ async function initUserStorage(dataRoot) {
430432 * Get the cookie secret from the config. If it doesn't exist, generate a new one.
431433 * @returns {string} The cookie secret
432434 */
433435export function getCookieSecret() {
434436 let secret = getConfigValue(STORAGE_KEYS.cookieSecret);
435437
436438 if (!secret) {
@@ -446,7 +448,7 @@ function getCookieSecret() {
446448 * Generates a random password salt.
447449 * @returns {string} The password salt
448450 */
449451export function getPasswordSalt() {
450452 return crypto.randomBytes(16).toString('base64');
451453}
452454
@@ -454,7 +456,7 @@ function getPasswordSalt() {
454456 * Get the session name for the current server.
455457 * @returns {string} The session name
456458 */
457459export function getCookieSessionName() {
458460 // Get server hostname and hash it to generate a session suffix
459461 const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8);
460462 return `session-${suffix}`;
@@ -466,7 +468,7 @@ function getCookieSessionName() {
466468 * @param {string} salt Salt to use for hashing
467469 * @returns {string} Hashed password
468470 */
469471export function getPasswordHash(password, salt) {
470472 return crypto.scryptSync(password.normalize(), salt, 64).toString('base64');
471473}
472474
@@ -475,7 +477,7 @@ function getPasswordHash(password, salt) {
475477 * @param {import('express').Request} [request] HTTP request object
476478 * @returns {string} The CSRF secret
477479 */
478480export function getCsrfSecret(request) {
479481 if (!request || !request.user) {
480482 return ANON_CSRF_SECRET;
481483 }
@@ -494,7 +496,7 @@ function getCsrfSecret(request) {
494496 * Gets a list of all user handles.
495497 * @returns {Promise<string[]>} - The list of user handles
496498 */
497499export async function getAllUserHandles() {
498500 const keys = await storage.keys(x => x.key.startsWith(KEY_PREFIX));
499501 const handles = keys.map(x => x.replace(KEY_PREFIX, ''));
500502 return handles;
@@ -505,7 +507,7 @@ async function getAllUserHandles() {
505507 * @param {string} handle User handle
506508 * @returns {UserDirectoryList} User directories
507509 */
508510export function getUserDirectories(handle) {
509511 if (DIRECTORIES_CACHE.has(handle)) {
510512 const cache = DIRECTORIES_CACHE.get(handle);
511513 if (cache) {
@@ -526,7 +528,7 @@ function getUserDirectories(handle) {
526528 * @param {string} handle User handle
527529 * @returns {Promise<string>} User avatar URL
528530 */
529531export async function getUserAvatar(handle) {
530532 try {
531533 // Check if the user has a custom avatar
532534 const avatarKey = toAvatarKey(handle);
@@ -563,7 +565,7 @@ async function getUserAvatar(handle) {
563565 * @param {import('express').Request} request Request object
564566 * @returns {boolean} Whether the user should be redirected to the login page
565567 */
566568export function shouldRedirectToLogin(request) {
567569 return ENABLE_ACCOUNTS && !request.user;
568570}
569571
@@ -574,7 +576,7 @@ function shouldRedirectToLogin(request) {
574576 * @param {boolean} basicAuthMode If Basic auth mode is enabled
575577 * @returns {Promise<boolean>} Whether auto-login was performed
576578 */
577579export async function tryAutoLogin(request, basicAuthMode) {
578580 if (!ENABLE_ACCOUNTS || request.user || !request.session) {
579581 return false;
580582 }
@@ -693,7 +695,7 @@ async function basicUserLogin(request) {
693695 * @param {import('express').Response} response Response object
694696 * @param {import('express').NextFunction} next Next function
695697 */
696698export async function setUserDataMiddleware(request, response, next) {
697699 // If user accounts are disabled, use the default user
698700 if (!ENABLE_ACCOUNTS) {
699701 const handle = DEFAULT_USER.handle;
@@ -751,7 +753,7 @@ async function setUserDataMiddleware(request, response, next) {
751753 * @param {import('express').Response} response Response object
752754 * @param {import('express').NextFunction} next Next function
753755 */
754756export function requireLoginMiddleware(request, response, next) {
755757 if (!request.user) {
756758 return response.sendStatus(403);
757759 }
@@ -787,7 +789,7 @@ function createRouteHandler(directoryFn) {
787789 * @param {import('express').NextFunction} next Next function
788790 * @returns {any}
789791 */
790792export function requireAdminMiddleware(request, response, next) {
791793 if (!request.user) {
792794 return response.sendStatus(403);
793795 }
@@ -806,7 +808,7 @@ function requireAdminMiddleware(request, response, next) {
806808 * @param {import('express').Response} response Express response object to write to
807809 * @returns {Promise<void>} Promise that resolves when the archive is created
808810 */
809811export async function createBackupArchive(handle, response) {
810812 const directories = getUserDirectories(handle);
811813
812814 console.log('Backup requested for', handle);
@@ -855,7 +857,7 @@ async function getAllUsers() {
855857 * Gets all of the enabled users.
856858 * @returns {Promise<User[]>}
857859 */
858860export async function getAllEnabledUsers() {
859861 const users = await getAllUsers();
860862 return users.filter(x => x.enabled);
861863}
@@ -863,7 +865,7 @@ async function getAllEnabledUsers() {
863865/**
864866 * Express router for serving files from the user's directories.
865867 */
866868export const router = express.Router();
867869router.use('/backgrounds/*', createRouteHandler(req => req.user.directories.backgrounds));
868870router.use('/characters/*', createRouteHandler(req => req.user.directories.characters));
869871router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.avatars));
@@ -871,31 +873,3 @@ router.use('/assets/*', createRouteHandler(req => req.user.directories.assets));
871873router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));
872874router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
873875router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions));
874-
875-module.exports = {
876- KEY_PREFIX,
877- toKey,
878- toAvatarKey,
879- initUserStorage,
880- ensurePublicDirectoriesExist,
881- getUserDirectoriesList,
882- getAllUserHandles,
883- getUserDirectories,
884- setUserDataMiddleware,
885- requireLoginMiddleware,
886- requireAdminMiddleware,
887- migrateUserData,
888- migrateSystemPrompts,
889- getPasswordSalt,
890- getPasswordHash,
891- getCsrfSecret,
892- getCookieSecret,
893- getCookieSessionName,
894- getUserAvatar,
895- shouldRedirectToLogin,
896- createBackupArchive,
897- tryAutoLogin,
898- getAllUsers,
899- getAllEnabledUsers,
900- router,
901-};
src/util.js+59 -81
@@ -1,13 +1,18 @@
11constimport path =from require('node:path');
22constimport fs =from require('node:fs');
3-const commandExistsSync = require('command-exists').sync;
3+import http2 from 'node:http2';
4-const writeFileAtomicSync = require('write-file-atomic').sync;
4+import process from 'node:process';
5-const _ = require('lodash');
5+import { Readable } from 'node:stream';
6-const yauzl = require('yauzl');
6+import { createRequire } from 'node:module';
7-const mime = require('mime-types');
7+import { Buffer } from 'node:buffer';
8-const yaml = require('yaml');
8+
9-const { default: simpleGit } = require('simple-git');
9+import yaml from 'yaml';
1010constimport { Readablesync as commandExistsSync } =from require('streamcommand-exists');
11+import { sync as writeFileAtomicSync } from 'write-file-atomic';
12+import _ from 'lodash';
13+import yauzl from 'yauzl';
14+import mime from 'mime-types';
15+import { default as simpleGit } from 'simple-git';
1116
1217/**
1318 * Parsed config object.
@@ -18,7 +23,7 @@ let CACHED_CONFIG = null;
1823 * Returns the config object from the config.yaml file.
1924 * @returns {object} Config object
2025 */
2126export function getConfig() {
2227 if (CACHED_CONFIG) {
2328 return CACHED_CONFIG;
2429 }
@@ -46,7 +51,7 @@ function getConfig() {
4651 * @param {any} defaultValue - Default value to return if the key is not found
4752 * @returns {any} Value for the given key
4853 */
4954export function getConfigValue(key, defaultValue = null) {
5055 const config = getConfig();
5156 return _.get(config, key, defaultValue);
5257}
@@ -56,7 +61,7 @@ function getConfigValue(key, defaultValue = null) {
5661 * @param {string} key Key to set
5762 * @param {any} value Value to set
5863 */
5964export function setConfigValue(key, value) {
6065 // Reset cache so that the next getConfig call will read the updated config file
6166 CACHED_CONFIG = null;
6267 const config = getConfig();
@@ -69,7 +74,7 @@ function setConfigValue(key, value) {
6974 * @param {string} auth username:password
7075 * @returns {string} Basic Auth header value
7176 */
7277export function getBasicAuthHeader(auth) {
7378 const encoded = Buffer.from(`${auth}`).toString('base64');
7479 return `Basic ${encoded}`;
7580}
@@ -79,7 +84,7 @@ function getBasicAuthHeader(auth) {
7984 * Also returns the agent string for the Horde API.
8085 * @returns {Promise<{agent: string, pkgVersion: string, gitRevision: string | null, gitBranch: string | null, commitDate: string | null, isLatest: boolean}>} Version info object
8186 */
8287export async function getVersion() {
8388 let pkgVersion = 'UNKNOWN';
8489 let gitRevision = null;
8590 let gitBranch = null;
@@ -87,9 +92,10 @@ async function getVersion() {
8792 let isLatest = true;
8893
8994 try {
95+ const require = createRequire(import.meta.url);
9096 const pkgJson = require(path.join(process.cwd(), './package.json'));
9197 pkgVersion = pkgJson.version;
9298 if (!process['pkg'] && commandExistsSync('git')) {
9399 const git = simpleGit();
94100 const cwd = process.cwd();
95101 gitRevision = await git.cwd(cwd).revparse(['--short', 'HEAD']);
@@ -117,7 +123,7 @@ async function getVersion() {
117123 * @param {number} ms Milliseconds to wait
118124 * @returns {Promise<void>} Promise that resolves after the given amount of milliseconds
119125 */
120126export function delay(ms) {
121127 return new Promise(resolve => setTimeout(resolve, ms));
122128}
123129
@@ -127,7 +133,7 @@ function delay(ms) {
127133 * @returns {string} Random hex string
128134 * @example getHexString(8) // 'a1b2c3d4'
129135 */
130136export function getHexString(length) {
131137 const chars = '0123456789abcdef';
132138 let result = '';
133139 for (let i = 0; i < length; i++) {
@@ -142,7 +148,7 @@ function getHexString(length) {
142148 * @param {string} fileExtension File extension to look for
143149 * @returns {Promise<Buffer|null>} Buffer containing the extracted file. Null if the file was not found.
144150 */
145151export async function extractFileFromZipBuffer(archiveBuffer, fileExtension) {
146152 return await new Promise((resolve, reject) => yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => {
147153 if (err) {
148154 reject(err);
@@ -181,7 +187,7 @@ async function extractFileFromZipBuffer(archiveBuffer, fileExtension) {
181187 * @param {string} zipFilePath Path to the ZIP archive
182188 * @returns {Promise<[string, Buffer][]>} Array of image buffers
183189 */
184190export async function getImageBuffers(zipFilePath) {
185191 return new Promise((resolve, reject) => {
186192 // Check if the zip file exists
187193 if (!fs.existsSync(zipFilePath)) {
@@ -237,7 +243,7 @@ async function getImageBuffers(zipFilePath) {
237243 * @param {any} readableStream Readable stream to read from
238244 * @returns {Promise<Buffer[]>} Array of chunks
239245 */
240246export async function readAllChunks(readableStream) {
241247 return new Promise((resolve, reject) => {
242248 // Consume the readable stream
243249 const chunks = [];
@@ -261,7 +267,7 @@ function isObject(item) {
261267 return (item && typeof item === 'object' && !Array.isArray(item));
262268}
263269
264270export function deepMerge(target, source) {
265271 let output = Object.assign({}, target);
266272 if (isObject(target) && isObject(source)) {
267273 Object.keys(source).forEach(key => {
@@ -278,7 +284,7 @@ function deepMerge(target, source) {
278284 return output;
279285}
280286
281287export const color = {
282288 byNum: (mess, fgNum) => {
283289 mess = mess || '';
284290 fgNum = fgNum === undefined ? 31 : fgNum;
@@ -298,7 +304,7 @@ const color = {
298304 * Gets a random UUIDv4 string.
299305 * @returns {string} A UUIDv4 string
300306 */
301307export function uuidv4() {
302308 if ('crypto' in global && 'randomUUID' in global.crypto) {
303309 return global.crypto.randomUUID();
304310 }
@@ -309,7 +315,7 @@ function uuidv4() {
309315 });
310316}
311317
312318export function humanizedISO8601DateTime(date) {
313319 let baseDate = typeof date === 'number' ? new Date(date) : new Date();
314320 let humanYear = baseDate.getFullYear();
315321 let humanMonth = (baseDate.getMonth() + 1);
@@ -322,7 +328,7 @@ function humanizedISO8601DateTime(date) {
322328 return HumanizedDateTime;
323329}
324330
325331export function tryParse(str) {
326332 try {
327333 return JSON.parse(str);
328334 } catch {
@@ -337,7 +343,7 @@ function tryParse(str) {
337343 * @param {string} inputPath The path to be converted.
338344 * @returns The relative URL path from which the client can access the file.
339345 */
340346export function clientRelativePath(root, inputPath) {
341347 if (!inputPath.startsWith(root)) {
342348 throw new Error('Input path does not start with the root directory');
343349 }
@@ -350,11 +356,11 @@ function clientRelativePath(root, inputPath) {
350356 * @param {string} filename The file name to remove the extension from.
351357 * @returns The file name, sans extension
352358 */
353359export function removeFileExtension(filename) {
354360 return filename.replace(/\.[^.]+$/, '');
355361}
356362
357363export function generateTimestamp() {
358364 const now = new Date();
359365 const year = now.getFullYear();
360366 const month = String(now.getMonth() + 1).padStart(2, '0');
@@ -371,7 +377,7 @@ function generateTimestamp() {
371377 * @param {string} directory The root directory to remove backups from.
372378 * @param {string} prefix File prefix to filter backups by.
373379 */
374380export function removeOldBackups(directory, prefix) {
375381 const MAX_BACKUPS = Number(getConfigValue('numberOfBackups', 50));
376382
377383 let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
@@ -389,7 +395,7 @@ function removeOldBackups(directory, prefix) {
389395 * @param {'name' | 'date'} sortBy Sort images by name or date
390396 * @returns {string[]} List of image file names
391397 */
392398export function getImages(directoryPath, sortBy = 'name') {
393399 function getSortFunction() {
394400 switch (sortBy) {
395401 case 'name':
@@ -415,7 +421,7 @@ function getImages(directoryPath, sortBy = 'name') {
415421 * @param {import('node-fetch').Response} from The Fetch API response to pipe from.
416422 * @param {import('express').Response} to The Express response to pipe to.
417423 */
418424export function forwardFetchResponse(from, to) {
419425 let statusCode = from.status;
420426 let statusText = from.statusText;
421427
@@ -434,17 +440,22 @@ function forwardFetchResponse(from, to) {
434440
435441 to.statusCode = statusCode;
436442 to.statusMessage = statusText;
437- from.body.pipe(to);
438443
439- to.socket.on('close', function () {
444+ if (from.body && to.socket) {
440- if (from.body instanceof Readable) from.body.destroy(); // Close the remote stream
445+ from.body.pipe(to);
441- to.end(); // End the Express response
446+
442- });
447+ to.socket.on('close', function () {
448+ if (from.body instanceof Readable) from.body.destroy(); // Close the remote stream
449+ to.end(); // End the Express response
450+ });
443451
444452 from.body.on('end', function () {
445453 console.log('Streaming request finished');
454+ to.end();
455+ });
456+ } else {
446457 to.end();
447458 });
448459}
449460
450461/**
@@ -457,10 +468,9 @@ function forwardFetchResponse(from, to) {
457468 * @param {object} headers Request headers
458469 * @returns {Promise<string>} Response body
459470 */
460471export function makeHttp2Request(endpoint, method, body, headers) {
461472 return new Promise((resolve, reject) => {
462473 try {
463- const http2 = require('http2');
464474 const url = new URL(endpoint);
465475 const client = http2.connect(url.origin);
466476
@@ -511,7 +521,7 @@ function makeHttp2Request(endpoint, method, body, headers) {
511521 * @param {string} yamlString YAML-serialized object
512522 * @returns
513523 */
514524export function mergeObjectWithYaml(obj, yamlString) {
515525 if (!yamlString) {
516526 return;
517527 }
@@ -540,7 +550,7 @@ function mergeObjectWithYaml(obj, yamlString) {
540550 * @param {string} yamlString YAML-serialized array
541551 * @returns {void} Nothing
542552 */
543553export function excludeKeysByYaml(obj, yamlString) {
544554 if (!yamlString) {
545555 return;
546556 }
@@ -569,14 +579,14 @@ function excludeKeysByYaml(obj, yamlString) {
569579 * @param {string} str Input string
570580 * @returns {string} Trimmed string
571581 */
572582export function trimV1(str) {
573583 return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, '');
574584}
575585
576586/**
577587 * Simple TTL memory cache.
578588 */
579589export class Cache {
580590 /**
581591 * @param {number} ttl Time to live in milliseconds
582592 */
@@ -633,7 +643,7 @@ class Cache {
633643 * @param {string} text Text with color formatting
634644 * @returns {string} Text without color formatting
635645 */
636646export function removeColorFormatting(text) {
637647 // ANSI escape codes for colors are usually in the format \x1b[<codes>m
638648 return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, '');
639649}
@@ -643,7 +653,7 @@ function removeColorFormatting(text) {
643653 * @param {number} n Number of times to repeat the separator
644654 * @returns {string} Separator string
645655 */
646656export function getSeparator(n) {
647657 return '='.repeat(n);
648658}
649659
@@ -652,7 +662,7 @@ function getSeparator(n) {
652662 * @param {string} url String to check
653663 * @returns {boolean} If the URL is valid
654664 */
655665export function isValidUrl(url) {
656666 try {
657667 new URL(url);
658668 return true;
@@ -660,35 +670,3 @@ function isValidUrl(url) {
660670 return false;
661671 }
662672}
663-
664-module.exports = {
665- getConfig,
666- getConfigValue,
667- setConfigValue,
668- getVersion,
669- getBasicAuthHeader,
670- extractFileFromZipBuffer,
671- getImageBuffers,
672- readAllChunks,
673- delay,
674- deepMerge,
675- color,
676- uuidv4,
677- humanizedISO8601DateTime,
678- tryParse,
679- clientRelativePath,
680- removeFileExtension,
681- generateTimestamp,
682- removeOldBackups,
683- getImages,
684- forwardFetchResponse,
685- getHexString,
686- mergeObjectWithYaml,
687- excludeKeysByYaml,
688- trimV1,
689- Cache,
690- makeHttp2Request,
691- removeColorFormatting,
692- getSeparator,
693- isValidUrl,
694-};
src/validator/TavernCardValidator.js+1 -3
@@ -5,7 +5,7 @@
55 *
66 * @link https://github.com/malfoyslastname/character-card-spec-v2
77 */
88export class TavernCardValidator {
99 /**
1010 * @type {string|null}
1111 */
@@ -167,5 +167,3 @@ class TavernCardValidator {
167167 return true;
168168 }
169169}
170-
171-module.exports = { TavernCardValidator };
src/vectors/cohere-vectors.js+7 -10
@@ -1,15 +1,15 @@
11constimport fetch =from require('node-fetch').default;
22constimport { SECRET_KEYS, readSecret } =from require('../endpoints/secrets.js');
33
44/**
55 * Gets the vector for the given text batch from an OpenAI compatible endpoint.
66 * @param {string[]} texts - The array of texts to get the vector for
77 * @param {boolean} isQuery - If the text is a query for embedding search
88 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
99 * @param {string} model - The model to use for the embedding
1010 * @returns {Promise<number[][]>} - The array of vectors for the texts
1111 */
1212export async function getCohereBatchVector(texts, isQuery, directories, model) {
1313 const key = readSecret(directories, SECRET_KEYS.COHERE);
1414
1515 if (!key) {
@@ -38,6 +38,7 @@ async function getCohereBatchVector(texts, isQuery, directories, model) {
3838 throw new Error('API request failed');
3939 }
4040
41+ /** @type {any} */
4142 const data = await response.json();
4243 if (!Array.isArray(data?.embeddings?.float)) {
4344 console.log('API response was not an array');
@@ -51,16 +52,12 @@ async function getCohereBatchVector(texts, isQuery, directories, model) {
5152 * Gets the vector for the given text from an OpenAI compatible endpoint.
5253 * @param {string} text - The text to get the vector for
5354 * @param {boolean} isQuery - If the text is a query for embedding search
5455 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
5556 * @param {string} model - The model to use for the embedding
5657 * @returns {Promise<number[]>} - The vector for the text
5758 */
5859export async function getCohereVector(text, isQuery, directories, model) {
5960 const vectors = await getCohereBatchVector([text], isQuery, directories, model);
6061 return vectors[0];
6162}
6263
63-module.exports = {
64- getCohereBatchVector,
65- getCohereVector,
66-};
src/vectors/embedding.js+4 -9
@@ -1,3 +1,4 @@
1+import { getPipeline } from '../transformers.mjs';
12const TASK = 'feature-extraction';
23
34/**
@@ -5,9 +6,8 @@ const TASK = 'feature-extraction';
56 * @param {string} text - The text to vectorize
67 * @returns {Promise<number[]>} - The vectorized text in form of an array of numbers
78 */
89export async function getTransformersVector(text) {
910 const modulepipe = await importgetPipeline('../transformers.mjs'TASK);
10- const pipe = await module.default.getPipeline(TASK);
1111 const result = await pipe(text, { pooling: 'mean', normalize: true });
1212 const vector = Array.from(result.data);
1313 return vector;
@@ -18,15 +18,10 @@ async function getTransformersVector(text) {
1818 * @param {string[]} texts - The texts to vectorize
1919 * @returns {Promise<number[][]>} - The vectorized texts in form of an array of arrays of numbers
2020 */
2121export async function getTransformersBatchVector(texts) {
2222 const result = [];
2323 for (const text of texts) {
2424 result.push(await getTransformersVector(text));
2525 }
2626 return result;
2727}
28-
29-module.exports = {
30- getTransformersVector,
31- getTransformersBatchVector,
32-};
src/vectors/extras-vectors.js+4 -8
@@ -1,4 +1,4 @@
11constimport fetch =from require('node-fetch').default;
22
33/**
44 * Gets the vector for the given text from SillyTavern-extras
@@ -7,7 +7,7 @@ const fetch = require('node-fetch').default;
77 * @param {string} apiKey - The Extras API key, or empty string if API key not enabled
88 * @returns {Promise<number[][]>} - The array of vectors for the texts
99 */
1010export async function getExtrasBatchVector(texts, apiUrl, apiKey) {
1111 return getExtrasVectorImpl(texts, apiUrl, apiKey);
1212}
1313
@@ -18,7 +18,7 @@ async function getExtrasBatchVector(texts, apiUrl, apiKey) {
1818 * @param {string} apiKey - The Extras API key, or empty string if API key not enabled
1919 * @returns {Promise<number[]>} - The vector for the text
2020 */
2121export async function getExtrasVector(text, apiUrl, apiKey) {
2222 return getExtrasVectorImpl(text, apiUrl, apiKey);
2323}
2424
@@ -66,13 +66,9 @@ async function getExtrasVectorImpl(text, apiUrl, apiKey) {
6666 throw new Error('Extras request failed');
6767 }
6868
69+ /** @type {any} */
6970 const data = await response.json();
7071 const vector = data.embedding; // `embedding`: number[] (one text item), or number[][] (multiple text items).
7172
7273 return vector;
7374}
74-
75-module.exports = {
76- getExtrasVector,
77- getExtrasBatchVector,
78-};
src/vectors/llamacpp-vectors.js+8 -12
@@ -1,15 +1,15 @@
11constimport fetch =from require('node-fetch').default;
22constimport { setAdditionalHeadersByType } =from require('../additional-headers.js');
33constimport { TEXTGEN_TYPES } =from require('../constants.js');
44
55/**
66 * Gets the vector for the given text from LlamaCpp
77 * @param {string[]} texts - The array of texts to get the vectors for
88 * @param {string} apiUrl - The API URL
99 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
1010 * @returns {Promise<number[][]>} - The array of vectors for the texts
1111 */
1212export async function getLlamaCppBatchVector(texts, apiUrl, directories) {
1313 const url = new URL(apiUrl);
1414 url.pathname = '/v1/embeddings';
1515
@@ -30,6 +30,7 @@ async function getLlamaCppBatchVector(texts, apiUrl, directories) {
3030 throw new Error(`LlamaCpp: Failed to get vector for text: ${response.statusText} ${responseText}`);
3131 }
3232
33+ /** @type {any} */
3334 const data = await response.json();
3435
3536 if (!Array.isArray(data?.data)) {
@@ -47,15 +48,10 @@ async function getLlamaCppBatchVector(texts, apiUrl, directories) {
4748 * Gets the vector for the given text from LlamaCpp
4849 * @param {string} text - The text to get the vector for
4950 * @param {string} apiUrl - The API URL
5051 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
5152 * @returns {Promise<number[]>} - The vector for the text
5253 */
5354export async function getLlamaCppVector(text, apiUrl, directories) {
5455 const vectors = await getLlamaCppBatchVector([text], apiUrl, directories);
5556 return vectors[0];
5657}
57-
58-module.exports = {
59- getLlamaCppBatchVector,
60- getLlamaCppVector,
61-};
src/vectors/makersuite-vectors.js+7 -11
@@ -1,14 +1,14 @@
11constimport fetch =from require('node-fetch').default;
22constimport { SECRET_KEYS, readSecret } =from require('../endpoints/secrets.js');
33const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
44
55/**
66 * Gets the vector for the given text from gecko model
77 * @param {string[]} texts - The array of texts to get the vector for
88 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
99 * @returns {Promise<number[][]>} - The array of vectors for the texts
1010 */
1111export async function getMakerSuiteBatchVector(texts, directories) {
1212 const promises = texts.map(text => getMakerSuiteVector(text, directories));
1313 return await Promise.all(promises);
1414}
@@ -16,10 +16,10 @@ async function getMakerSuiteBatchVector(texts, directories) {
1616/**
1717 * Gets the vector for the given text from Gemini API text-embedding-004 model
1818 * @param {string} text - The text to get the vector for
1919 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
2020 * @returns {Promise<number[]>} - The vector for the text
2121 */
2222export async function getMakerSuiteVector(text, directories) {
2323 const key = readSecret(directories, SECRET_KEYS.MAKERSUITE);
2424
2525 if (!key) {
@@ -52,12 +52,8 @@ async function getMakerSuiteVector(text, directories) {
5252 throw new Error('Google AI Studio request failed');
5353 }
5454
55+ /** @type {any} */
5556 const data = await response.json();
5657 // noinspection JSValidateTypes
5758 return data['embedding']['values'];
5859}
59-
60-module.exports = {
61- getMakerSuiteVector,
62- getMakerSuiteBatchVector,
63-};
src/vectors/nomicai-vectors.js+7 -11
@@ -1,5 +1,5 @@
11constimport fetch =from require('node-fetch').default;
22constimport { SECRET_KEYS, readSecret } =from require('../endpoints/secrets.js');
33
44const SOURCES = {
55 'nomicai': {
@@ -13,10 +13,10 @@ const SOURCES = {
1313 * Gets the vector for the given text batch from an OpenAI compatible endpoint.
1414 * @param {string[]} texts - The array of texts to get the vector for
1515 * @param {string} source - The source of the vector
1616 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
1717 * @returns {Promise<number[][]>} - The array of vectors for the texts
1818 */
1919export async function getNomicAIBatchVector(texts, source, directories) {
2020 const config = SOURCES[source];
2121
2222 if (!config) {
@@ -51,6 +51,7 @@ async function getNomicAIBatchVector(texts, source, directories) {
5151 throw new Error('API request failed');
5252 }
5353
54+ /** @type {any} */
5455 const data = await response.json();
5556 if (!Array.isArray(data?.embeddings)) {
5657 console.log('API response was not an array');
@@ -64,15 +65,10 @@ async function getNomicAIBatchVector(texts, source, directories) {
6465 * Gets the vector for the given text from an OpenAI compatible endpoint.
6566 * @param {string} text - The text to get the vector for
6667 * @param {string} source - The source of the vector
6768 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
6869 * @returns {Promise<number[]>} - The vector for the text
6970 */
7071export async function getNomicAIVector(text, source, directories) {
7172 const vectors = await getNomicAIBatchVector([text], source, directories);
7273 return vectors[0];
7374}
74-
75-module.exports = {
76- getNomicAIVector,
77- getNomicAIBatchVector,
78-};
src/vectors/ollama-vectors.js+8 -12
@@ -1,6 +1,6 @@
11constimport fetch =from require('node-fetch').default;
22constimport { setAdditionalHeadersByType } =from require('../additional-headers.js');
33constimport { TEXTGEN_TYPES } =from require('../constants.js');
44
55/**
66 * Gets the vector for the given text from Ollama
@@ -8,10 +8,10 @@ const { TEXTGEN_TYPES } = require('../constants');
88 * @param {string} apiUrl - The API URL
99 * @param {string} model - The model to use
1010 * @param {boolean} keep - Keep the model loaded in memory
1111 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
1212 * @returns {Promise<number[][]>} - The array of vectors for the texts
1313 */
1414export async function getOllamaBatchVector(texts, apiUrl, model, keep, directories) {
1515 const result = [];
1616 for (const text of texts) {
1717 const vector = await getOllamaVector(text, apiUrl, model, keep, directories);
@@ -26,10 +26,10 @@ async function getOllamaBatchVector(texts, apiUrl, model, keep, directories) {
2626 * @param {string} apiUrl - The API URL
2727 * @param {string} model - The model to use
2828 * @param {boolean} keep - Keep the model loaded in memory
2929 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
3030 * @returns {Promise<number[]>} - The vector for the text
3131 */
3232export async function getOllamaVector(text, apiUrl, model, keep, directories) {
3333 const url = new URL(apiUrl);
3434 url.pathname = '/api/embeddings';
3535
@@ -54,6 +54,7 @@ async function getOllamaVector(text, apiUrl, model, keep, directories) {
5454 throw new Error(`Ollama: Failed to get vector for text: ${response.statusText} ${responseText}`);
5555 }
5656
57+ /** @type {any} */
5758 const data = await response.json();
5859
5960 if (!Array.isArray(data?.embedding)) {
@@ -62,8 +63,3 @@ async function getOllamaVector(text, apiUrl, model, keep, directories) {
6263
6364 return data.embedding;
6465}
65-
66-module.exports = {
67- getOllamaBatchVector,
68- getOllamaVector,
69-};
src/vectors/openai-vectors.js+7 -11
@@ -1,5 +1,5 @@
11constimport fetch =from require('node-fetch').default;
22constimport { SECRET_KEYS, readSecret } =from require('../endpoints/secrets.js');
33
44const SOURCES = {
55 'togetherai': {
@@ -23,11 +23,11 @@ const SOURCES = {
2323 * Gets the vector for the given text batch from an OpenAI compatible endpoint.
2424 * @param {string[]} texts - The array of texts to get the vector for
2525 * @param {string} source - The source of the vector
2626 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
2727 * @param {string} model - The model to use for the embedding
2828 * @returns {Promise<number[][]>} - The array of vectors for the texts
2929 */
3030export async function getOpenAIBatchVector(texts, source, directories, model = '') {
3131 const config = SOURCES[source];
3232
3333 if (!config) {
@@ -61,6 +61,7 @@ async function getOpenAIBatchVector(texts, source, directories, model = '') {
6161 throw new Error('API request failed');
6262 }
6363
64+ /** @type {any} */
6465 const data = await response.json();
6566
6667 if (!Array.isArray(data?.data)) {
@@ -79,16 +80,11 @@ async function getOpenAIBatchVector(texts, source, directories, model = '') {
7980 * Gets the vector for the given text from an OpenAI compatible endpoint.
8081 * @param {string} text - The text to get the vector for
8182 * @param {string} source - The source of the vector
8283 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
8384 * @param {string} model - The model to use for the embedding
8485 * @returns {Promise<number[]>} - The vector for the text
8586 */
8687export async function getOpenAIVector(text, source, directories, model = '') {
8788 const vectors = await getOpenAIBatchVector([text], source, directories, model);
8889 return vectors[0];
8990}
90-
91-module.exports = {
92- getOpenAIVector,
93- getOpenAIBatchVector,
94-};
src/vectors/vllm-vectors.js+8 -12
@@ -1,16 +1,16 @@
11constimport fetch =from require('node-fetch').default;
22constimport { setAdditionalHeadersByType } =from require('../additional-headers.js');
33constimport { TEXTGEN_TYPES } =from require('../constants.js');
44
55/**
66 * Gets the vector for the given text from VLLM
77 * @param {string[]} texts - The array of texts to get the vectors for
88 * @param {string} apiUrl - The API URL
99 * @param {string} model - The model to use
1010 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
1111 * @returns {Promise<number[][]>} - The array of vectors for the texts
1212 */
1313export async function getVllmBatchVector(texts, apiUrl, model, directories) {
1414 const url = new URL(apiUrl);
1515 url.pathname = '/v1/embeddings';
1616
@@ -31,6 +31,7 @@ async function getVllmBatchVector(texts, apiUrl, model, directories) {
3131 throw new Error(`VLLM: Failed to get vector for text: ${response.statusText} ${responseText}`);
3232 }
3333
34+ /** @type {any} */
3435 const data = await response.json();
3536
3637 if (!Array.isArray(data?.data)) {
@@ -49,15 +50,10 @@ async function getVllmBatchVector(texts, apiUrl, model, directories) {
4950 * @param {string} text - The text to get the vector for
5051 * @param {string} apiUrl - The API URL
5152 * @param {string} model - The model to use
5253 * @param {import('../users.js').UserDirectoryList} directories - The directories object for the user
5354 * @returns {Promise<number[]>} - The vector for the text
5455 */
5556export async function getVllmVector(text, apiUrl, model, directories) {
5657 const vectors = await getVllmBatchVector([text], apiUrl, model, directories);
5758 return vectors[0];
5859}
59-
60-module.exports = {
61- getVllmBatchVector,
62- getVllmVector,
63-};