Merge branch 'staging' into release

This commit is contained in:
Cohee 2023-07-29 22:58:58 +03:00
commit 675cabb7e3
53 changed files with 3367 additions and 616 deletions

306
.github/readme-zh_cn.md vendored Normal file
View File

@ -0,0 +1,306 @@
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b)
移动设备界面友好多种人工智能服务或模型支持KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI+proxies, WindowAI(Claude!)),类似 Galgame 的 老 婆 模 式Horde SD文本系统语音生成世界信息Lorebooks可定制的界面自动翻译和比你所需要的更多的 Prompt。附带扩展服务支持文本绘画生成与语音生成和基于向量数据库 ChromaDB 的聊天信息总结。
基于 TavernAI 1.2.8 的分叉版本
### 由 Cohee、RossAscends 和 SillyTavern 社区为您呈现
注意:我们创建了一个 [帮助文档](https://docs.sillytavern.app/) 网站来回答各类问题与帮助您开始使用。
### SillyTavern 或 TavernAI 是什么?
SillyTavern 是一个可以安装在电脑(和安卓手机)上的用户界面,让您可以与文本生成的人工智能互动,并与您或社区创建的角色聊天/玩角色扮演游戏。
SillyTavern 是 TavernAI 1.2.8 的一个分支,正在进行更积极地开发,并添加了许多重要功能。在这一点上,它可以被视为完全独立的程序。
### 分支
SillyTavern 采用双分支进行开发,以确保所有用户都能获得流畅的使用体验。
* release -🌟 **推荐给大多数用户。** 这是最稳定、最推荐的分支,只有在重大版本推送时才会更新。适合大多数用户使用。
* staging - ⚠️ **不建议随意使用。** 该分支拥有最新功能,但要谨慎,因为它随时可能崩溃。仅适用于高级用户和爱好者。
如果你不熟悉使用 Git 命令或者不了解什么是分支别担心release 分支始终是您的首选。
### 除了 SillyTavern我还需要什么
SillyTavern 本身并无用处因为它只是一个用户聊天界面。你必须接入一个能充当角色扮演的人工智能系统。支持的人工智能系统有多种OpenAPI API (GPT)、KoboldAI可在本地或 Google Colab 上运行)等。您可以在 [常见问题](https://docs.sillytavern.app/usage/faq/) 中阅读更多相关信息。
### 我需要一台性能强大的电脑来运行 SillyTavern 吗?
由于 SillyTavern 只是一个用户聊天界面,它对硬件性能的要求很低,可以在任何电脑上运行。需要强大性能的是人工智能系统。
### 移动设备支持
> 注意
> **此分叉可使用 Termux 在安卓手机上原生运行。请参考 ArroganceComplex#2659 编写的指南:**
<https://rentry.org/STAI-Termux>
Termux 不支持**.Webp 字符卡的导入/导出。请使用 JSON 或 PNG 格式**。
## 有问题或建议?
### 我们现在有了 Discord 社区
获取支持,或分享喜爱的角色和 Prompt
### [加入 Discord 社区](https://discord.gg/RZdyAEUPvj)
***
直接与开发人员联系:
* Discord: cohee 或 rossascends
* Reddit/u/RossAscends 或 /u/sillylossy
* [发布 GitHub 问题](https://github.com/SillyTavern/SillyTavern/issues)
## 此版本包括
* 经过大量修改的 TavernAI 1.2.8(超过 50% 的代码经过重写或优化)
* 根据自定义规则自动重新生成消息
* 群聊:多机器人房间,供角色与你或彼此交谈
* 聊天书签/分支(复制当前状态下的对话)
* 先进的 KoboldAI / TextGen 生成设置,包含大量社区预设
* 支持世界信息Lorebooks创建丰富的传说
* 支持 Window AI 浏览器扩展(运行 Claude、GPT 4 等模型):<https://windowai.io/>
* [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui) API 连接
* 连接 [AI Horde](https://horde.koboldai.net/)
* Prompt 生成格式调整
* Webp 角色卡支持PNG 仍是内部格式)
## 扩展
SillyTavern 支持扩展服务,一些额外的人工智能模块可通过 [SillyTavern Extras API](https://github.com/SillyTavern/SillyTavern-extras) 提供。
* 作者注释/角色偏见
* 角色情绪识别
* 聊天记录自动摘要
* 在聊天窗口发送图片,并由人工智能解释图片内容
* 文本图像生成5 预设,以及 "自由模式"
* 聊天信息的文字转语音(通过 ElevenLabs、Silero 或操作系统的语音生成)
* ChromaDB 向量数据库,用于更智能的聊天 Prompt
扩展服务的完整功能介绍和使用教程,请参阅 [Docs](https://docs.sillytavern.app/extras/extensions/)。
## 界面/CSS/性能,由 RossAscends 调整并优化
* 针对 iOS 系统优化了界面,并支持将快捷方式保存到主屏幕,在全屏模式下打开。
* 热键
* 上 = 编辑聊天中的最后一条信息
* Ctrl+P = 编辑聊天中最后一条用户信息
* 左 = 向左滑动
* 右 = 向右滑动(注意:当聊天窗口输入内容时,轻扫快捷键将被禁用)
* Ctrl+左 = 查看本地存储的变量(在浏览器控制台窗口中)
* 回车(选择聊天栏)= 向人工智能发送信息
* Ctrl+Enter = 重新生成人工智能最后的回复
* 用户名更改和角色删除不再强制重新刷新页面。
* 增加在页面加载时自动连接 API 的选项。
* 增加选项,在页面加载时自动加载最近的聊天信息。
* 更好的 Tokens 计算器 - 适用于未保存的文字,并显示永久和临时 Tokens 数量
* 更好的聊天历史查询窗口
* 聊天的文件名以"(角色卡名称)+(创建时间)"的可读格式保存
* 聊天历史预览从 40 个字符增加到 300 个字符。
* 聊天历史排序有多种选择(按名称、创建日期、聊天记录大小)。
* 默认情况下,左侧和右侧弹出的设置面板会在点击其他区域时自动关闭。
* 点击导航面板上的 "锁按钮" 将保持弹出面板打开,并在不同聊天中记住此设置。
* 导航面板的打开或关闭状态也会跨聊天保存。
* 自定义聊天界面:
* 收到新消息时播放提示音
* 切换圆形或长方形头像样式
* 在台式电脑上拥有更宽的聊天窗口
* 可选的半透明玻璃效果聊天窗口
* 可定制 "主文本"、"引用文本 "和 "斜体文本 "的字体颜色。
* 可定制聊天界面的背景颜色和透明模糊程度
## 安装
*注意SillyTavern 用于本地安装,尚未在 Colab 或其他云服务上进行全面测试。
> **警告**
> 切勿安装到任何受 Windows 控制的系统文件夹Program Files, System32, etc中。
> 不要以管理员权限运行 start.bat
### Windows
通过 Git 安装(推荐使用,便于更新)
附有精美图片示例的简易指南:
<https://docs.sillytavern.app/installation/windows/>
1. 安装 [NodeJS](https://nodejs.org/en)(建议使用最新的 LTS 版本)
2. 安装 [GitHub 客户端](https://central.github.com/deployments/desktop/desktop/latest/win32)
3. 打开 Windows 资源管理器 (`Win+E`)
4. 浏览或创建一个不受 Windows 控制或监控的文件夹。(例如C:\MySpecialFolder\)
5. 点击顶部的 "地址栏",在该文件夹内打开命令提示符,输入 `cmd`,然后按回车。
6. 弹出黑框CMD 命令提示符)后,键入以下其中一项并按 Enter
* 稳定分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
* 开发分支: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
7. 等待 Git 克隆完成后,双击文件夹中的 `Start.bat` 将启动 NodeJS 并开始自动安装需要的软件包。
8. 然后 SillyTavern 服务就会自动启动,同时在浏览器新标签页中自动打开。
通过压缩包下载安装(不推荐)
1. 安装 [NodeJS](https://nodejs.org/en)(建议使用最新的 LTS 版本)
2. 从该 GitHub 仓库下载压缩包。(从 [Releases](https://github.com/SillyTavern/SillyTavern/releases/latest) 获取 "Source codezip")。
3. 将压缩包解压到您选择的文件夹中
4. 双击或在命令行中运行 `Start.bat`
5. SillyTavern 服务自动为你准备好一切后,会在你的浏览器中打开一个新标签页。
### Linux
1.运行 `start.sh` 脚本。
2.等待自动完成,然后开始享受
## API 密钥管理
SillyTavern 会将 API 密钥保存在目录中的 `secrets.json` 文件内。
默认情况下,输入密钥并重新加载页面后,密钥会自动隐藏以保证安全。
如果要想通过点击 API 输入框旁边的按钮来查看密钥,请按照以下设置:
1. 打开 `config.conf` 文件,将里面的 `allowKeysExposure` 设置为 `true`
2. 然后重启 SillyTavern 服务。
## 远程访问
这通常是为那些想在手机上使用 SillyTavern 的人准备的,而他们的电脑和手机在同一个局域网中。
不过SillyTavern 也可以被设置为允许从任何地方进行远程访问。
**重要提示SillyTavern 是单用户程序,因此任何人登录后都能看到所有的角色卡和聊天内容,并能更改任何设置。
### 1.管理白名单 IP
* 在你的 SillyTavern 文件夹中新建一个文本文件,名为 `whitelist.txt`
* 用文本编辑器打开该文件,添加你希望允许连接的 IP 地址列表。
* 接受单个 IP 地址和 IP 范围,示例:
```
192.168.0.1
192.168.0.20
```
或者
```
192.168.0.*
```
(上述 IP 范围将允许局域网中的任何设备连接)
也接受子网掩码设置(如 10.0.0.0/24
* 保存`whitelist.txt`文件。
* 重启 SillyTavern 服务。
然后,文件中设置的 IP 就可以访问 SillyTavern 了。
*注意:"config.conf" 文件内也有一个 "whitelist" 设置,你可以用同样的方法设置它,但如果 "whitelist.txt" 文件存在,这个设置将被忽略。
### 2.获取 SillyTavern 服务的 IP 地址
白名单设置完成后,您需要 SillyTavern 服务的 IP 地址。
如果 SillyTavern 服务设备在同一个局域网上,则使用安装 SillyTavern 服务的电脑的局域网 IP 地址:
* WindowsWindows 按钮 > 在搜索栏中输入 `cmd.exe` > 在打开的控制台中输入 `ipconfig`,回车 > 然后在输出中查找 `IPv4` 地址。
如果您(或其他人)想在互联网中访问你自己的 SillyTavern 服务,则需要运行 SillyTavern 服务的设备的互联网 IP 地址。
* 使用运行 SillyTavern 的设备,访问 [this page](https://whatismyipaddress.com/) 并查找 `IPv4`。这是您从互联网访问时要用到的。
### 3. 使用其他设备访问 SillyTavern 服务
无论你最终使用的是什么 IP 地址,都要将该 IP 地址和端口号输入其他设备网络浏览器。
同一局域网中的 SillyTavern 服务的典型默认地址如下:
`http://192.168.0.5:8000`
使用 http:// 而不是 https://
### 向所有 IP 开放您的 SillyTavern 服务
我们不建议这样做,但您可以打开 `config.conf` 并将里面的 `whitelist` 设置改为 `false`
你必须删除或重命名SillyTavern 文件夹中的 `whitelist.txt` 文件(如果有的话)。
这通常是不安全的做法,所以我们要求在这样做时必须设置用户名和密码。
用户名和密码在`config.conf`文件中设置。
重启 SillyTavern 服务后,只要知道用户名和密码,任何设备都可以访问。
### 还是无法访问?
* 为 `config.conf` 文件中的端口创建一条入站/出站防火墙规则。切勿将此误认为是路由器上的端口转发,否则,有人可能会发现你的聊天隐私,那就大错特错了。
* 在 "设置" > "网络和 Internet" > "以太网" 中启用 "专用网络" 配置。这对 Windows 11 非常重要,否则即使添加了上述防火墙规则也无法连接。
### 性能问题?
尝试在用户设置面板上关闭模糊效果(快速用户界面)模式。
## 我喜欢你的项目!我该如何贡献自己的力量?
### 应该
1. 发送 Fork 请求
2. 使用规定的模板发送功能建议和问题报告
3. 在提出任何问题之前,请先阅读 Readme 文件和文档
#### 不应该
1. 提供金钱捐赠
2. 发送错误报告而不提供任何详细信息
3. 提出已经回答过无数次的问题
## 我在哪里可以找到以前的聊天背景图片?
我们正在实行 100% 原创内容的政策,因此旧的背景图片已从该资源库中删除。
不过你可以在这里找到它们的存档:
<https://files.catbox.moe/1xevnc.zip>
## 屏幕截图
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649245-8061c60f-63dc-488e-9325-f151b7a3ec2d.png">
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649856-fbdeef05-d727-4d5a-be80-266cbbc6b811.png">
## 许可证和贡献
** 发布本程序是希望它能有所帮助,但不做任何保证;甚至没有明示的性能、稳定性和其他任何特定用途的可用性保证。更多详情,请参阅 GNU Affero 通用公共许可证。 **
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. **
* TAI Base by Humi: Unknown license
* Cohee's modifications and derived code: AGPL v3
* RossAscends' additions: AGPL v3
* Portions of CncAnon's TavernAITurbo mod: Unknown license
* kingbri's various commits and suggestions (https://github.com/bdashore3)
* BlipRanger's miscellaneous UI & extension modifications (https://github.com/BlipRanger)
* Waifu mode inspired by the work of PepperTaco (https://github.com/peppertaco/Tavern/)
* Thanks Pygmalion University for being awesome testers and suggesting cool features!
* Thanks oobabooga for compiling presets for TextGen
* KoboldAI Presets from KAI Lite: https://lite.koboldai.net/
* Noto Sans font by Google (OFL license)
* Icon theme by Font Awesome https://fontawesome.com (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* AI Horde client library by ZeldaFan0225: https://github.com/ZeldaFan0225/ai_horde
* Linux startup script by AlpinDale
* Thanks paniphons for providing a FAQ document
* 10K Discord Users Celebratory Background by @kallmeflocc
* Default content (characters and lore books) provided by @OtisAlejandro, @RossAscends and @kallmeflocc
* Korean translation by @doloroushyeonse

View File

@ -1,9 +1,9 @@
name: Build and Publish Release (Dev)
name: Build and Publish Release (Release)
on:
push:
branches:
- dev
- release
jobs:
build_and_publish:
@ -30,8 +30,8 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: dist/*
tag_name: ci-dev
name: Continuous Release (Dev)
tag_name: ci-release
name: Continuous Release (Release)
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,9 +1,9 @@
name: Build and Publish Release (Main)
name: Build and Publish Release (Staging)
on:
push:
branches:
- main
- staging
jobs:
build_and_publish:
@ -30,8 +30,8 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: dist/*
tag_name: ci-main
name: Continuous Release (Main)
tag_name: ci-staging
name: Continuous Release (Staging)
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@ -9,6 +9,8 @@ public/worlds/
public/css/bg_load.css
public/themes/
public/OpenAI Settings/
public/KoboldAI Settings/
public/TextGen Settings/
public/scripts/extensions/third-party/
public/stats.json
/uploads/

View File

@ -4,7 +4,7 @@ FROM node:19.1.0-alpine3.16
ARG APP_HOME=/home/node/app
# Install system dependencies
RUN apk add gcompat tini
RUN apk add gcompat tini git
# Ensure proper handling of kernel signals
ENTRYPOINT [ "tini", "--" ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 338 KiB

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.9.2",
"version": "1.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.9.2",
"version": "1.9.3",
"license": "AGPL-3.0",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",

View File

@ -51,7 +51,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.9.2",
"version": "1.9.3",
"scripts": {
"start": "node server.js",
"pkg": "pkg --compress Gzip --no-bytecode --public ."

View File

@ -1,6 +1,6 @@
{
"max_length": 100,
"genamt": 100,
"max_length": 2048,
"genamt": 200,
"temp": 1,
"top_k": 0,
"top_p": 0.95,
@ -19,4 +19,4 @@
4,
5
]
}
}

View File

@ -0,0 +1,18 @@
{
"order": [5, 0, 1, 3],
"temperature": 1.23,
"max_length": 300,
"min_length": 1,
"top_k": 200,
"typical_p": 0.966,
"tail_free_sampling": 0.982,
"repetition_penalty": 1.74,
"repetition_penalty_range": 4000,
"repetition_penalty_frequency": 0,
"repetition_penalty_presence": 0.02,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "aggressive",
"max_context": 8192
}

View File

@ -0,0 +1,19 @@
{
"order": [6, 0, 1, 2, 3],
"temperature": 1,
"max_length": 300,
"min_length": 1,
"top_k": 25,
"top_p": 1,
"tail_free_sampling": 0.925,
"repetition_penalty": 1.6,
"repetition_penalty_frequency": 0.001,
"repetition_penalty_range": 0,
"repetition_penalty_presence": 0,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "medium",
"cfg_scale": 1.55,
"max_context": 8192
}

View File

@ -0,0 +1,20 @@
{
"order": [6, 2, 3, 1, 0],
"temperature": 1,
"max_length": 300,
"min_length": 1,
"top_k": 0,
"top_p": 0.96,
"tail_free_sampling": 0.96,
"repetition_penalty": 2,
"repetition_penalty_slope": 1,
"repetition_penalty_frequency": 0.02,
"repetition_penalty_range": 0,
"repetition_penalty_presence": 0.3,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "very_aggressive",
"cfg_scale": 1.3,
"max_context": 8192
}

View File

@ -0,0 +1,20 @@
{
"order": [2, 3, 0, 4, 1],
"temperature": 1.35,
"max_length": 300,
"min_length": 1,
"top_k": 12,
"top_p": 0.85,
"top_a": 0.1,
"tail_free_sampling": 0.915,
"repetition_penalty": 2.8,
"repetition_penalty_range": 2048,
"repetition_penalty_slope": 0.02,
"repetition_penalty_frequency": 0.02,
"repetition_penalty_presence": 0,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "aggressive",
"max_context": 8192
}

View File

@ -14,7 +14,10 @@
"repetition_penalty_range": 8192,
"repetition_penalty_frequency": 0.03,
"repetition_penalty_presence": 0.005,
"repetition_penalty_slope": 0,
"top_a": 0.075,
"top_k": 79,
"top_p": 0.95
"top_p": 0.95,
"typical_p": 1,
"max_context": 8192
}

View File

@ -0,0 +1,20 @@
{
"order": [4, 0, 5, 3, 2],
"temperature": 1.09,
"max_length": 300,
"min_length": 1,
"top_p": 0.969,
"top_a": 0.09,
"typical_p": 0.99,
"tail_free_sampling": 0.969,
"repetition_penalty": 1.09,
"repetition_penalty_range": 8192,
"repetition_penalty_slope": 0.069,
"repetition_penalty_frequency": 0.006,
"repetition_penalty_presence": 0.009,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "very_light",
"max_context": 8192
}

View File

@ -5,6 +5,8 @@
"min_length": 1,
"top_k": 25,
"top_p": 1,
"top_a": 0,
"typical_p": 1,
"tail_free_sampling": 0.925,
"repetition_penalty": 1.9,
"repetition_penalty_range": 768,
@ -15,4 +17,4 @@
"return_full_text": false,
"prefix": "vanilla",
"max_context": 8192
}
}

View File

@ -0,0 +1,19 @@
{
"order": [6, 0, 1, 2, 3],
"temperature": 1,
"max_length": 300,
"min_length": 1,
"top_k": 25,
"top_p": 1,
"tail_free_sampling": 0.925,
"repetition_penalty": 1.9,
"repetition_penalty_range": 768,
"repetition_penalty_slope": 1,
"repetition_penalty_frequency": 0.0025,
"repetition_penalty_presence": 0.001,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "off",
"max_context": 8192
}

View File

@ -0,0 +1,19 @@
{
"order": [6, 1, 0, 5, 3],
"temperature": 1.25,
"max_length": 300,
"min_length": 1,
"top_k": 70,
"typical_p": 0.9,
"tail_free_sampling": 0.925,
"repetition_penalty": 2,
"repetition_penalty_range": 1632,
"repetition_penalty_frequency": 0,
"repetition_penalty_presence": 0,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "aggressive",
"cfg_scale": 1.825,
"max_context": 8192
}

View File

@ -4,6 +4,8 @@
"max_length": 40,
"min_length": 1,
"top_a": 0.022,
"top_k": 0,
"top_p": 1,
"typical_p": 0.9,
"tail_free_sampling": 0.956,
"repetition_penalty": 1.25,
@ -15,4 +17,4 @@
"return_full_text": false,
"prefix": "vanilla",
"max_context": 8192
}
}

View File

@ -5,6 +5,7 @@
"min_length": 1,
"top_k": 25,
"top_a": 0.3,
"top_p": 1,
"typical_p": 0.96,
"tail_free_sampling": 0.895,
"repetition_penalty": 1.0125,
@ -16,4 +17,4 @@
"return_full_text": false,
"prefix": "vanilla",
"max_context": 8192
}
}

View File

@ -0,0 +1,22 @@
{
"order": [6, 0, 4, 1, 2, 5, 3],
"temperature": 1.31,
"max_length": 300,
"min_length": 1,
"top_k": 25,
"top_p": 0.97,
"top_a": 0.18,
"typical_p": 0.98,
"tail_free_sampling": 1,
"repetition_penalty": 1.55,
"repetition_penalty_frequency": 0.00075,
"repetition_penalty_presence": 0.00085,
"repetition_penalty_range": 8192,
"repetition_penalty_slope": 1.8,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "medium",
"cfg_scale": 1.35,
"max_context": 8192
}

View File

@ -0,0 +1,17 @@
{
"order": [3, 0, 5],
"temperature": 2.5,
"max_length": 300,
"min_length": 1,
"typical_p": 0.966,
"tail_free_sampling": 0.933,
"repetition_penalty": 1,
"repetition_penalty_range": 2048,
"repetition_penalty_frequency": 0,
"repetition_penalty_presence": 0,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "aggressive",
"max_context": 8192
}

View File

@ -1,19 +1,21 @@
{
"order": [1, 3, 4, 0, 2],
"temperature": 1.05,
"max_length": 40,
"order": [1, 5, 0, 2, 3, 4],
"temperature": 1.5,
"max_length": 300,
"min_length": 1,
"top_k": 79,
"top_p": 0.95,
"top_a": 0.075,
"tail_free_sampling": 0.989,
"repetition_penalty": 1.5,
"top_k": 10,
"top_p": 0.75,
"top_a": 0.08,
"typical_p": 0.975,
"tail_free_sampling": 0.967,
"repetition_penalty": 2.25,
"repetition_penalty_range": 8192,
"repetition_penalty_slope": 3.33,
"repetition_penalty_frequency": 0.03,
"repetition_penalty_slope": 0.09,
"repetition_penalty_frequency": 0,
"repetition_penalty_presence": 0.005,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "very_light",
"max_context": 8192
}
}

View File

@ -0,0 +1,18 @@
{
"order": [6, 0, 5],
"temperature": 0.895,
"max_length": 300,
"min_length": 1,
"typical_p": 0.9,
"repetition_penalty": 2,
"repetition_penalty_slope": 3.2,
"repetition_penalty_frequency": 0,
"repetition_penalty_presence": 0,
"repetition_penalty_range": 4048,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "aggressive",
"cfg_scale": 1.3,
"max_context": 8192
}

View File

@ -5,6 +5,7 @@
"min_length": 1,
"top_k": 0,
"top_p": 0.912,
"top_a": 1,
"typical_p": 0.912,
"tail_free_sampling": 0.921,
"repetition_penalty": 1.21,
@ -16,4 +17,4 @@
"return_full_text": false,
"prefix": "vanilla",
"max_context": 8192
}
}

View File

@ -0,0 +1,19 @@
{
"order": [6, 1, 0, 5, 3, 2],
"temperature": 1.5,
"max_length": 300,
"min_length": 1,
"top_k": 70,
"top_p": 0.95,
"typical_p": 0.95,
"tail_free_sampling": 0.95,
"repetition_penalty": 1.6,
"repetition_penalty_range": 2016,
"repetition_penalty_frequency": 0,
"repetition_penalty_presence": 0,
"use_cache": false,
"return_full_text": false,
"prefix": "vanilla",
"phrase_rep_pen": "very_aggressive",
"max_context": 8192
}

View File

@ -95,6 +95,7 @@
<script type="module" src="scripts/context-template.js"></script>
<script type="module" src="scripts/extensions.js"></script>
<script type="module" src="scripts/authors-note.js"></script>
<script type="module" src="scripts/preset-manager.js"></script>
<script type="text/javascript" src="scripts/toolcool-color-picker.js"></script>
<title>SillyTavern</title>
@ -131,15 +132,24 @@
<div class="scrollableInner">
<div class="flex-container" id="ai_response_configuration">
<div id="respective-presets-block" class="width100p">
<input type="file" hidden data-preset-manager-file="" accept=".json, .settings">
<div id="kobold_api-presets">
<h3><span data-i18n="kobldpresets">Kobold Presets</span>
<a href="https://docs.sillytavern.app/usage/api-connections/koboldai/" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
</h3>
<select id="settings_perset">
<option value="gui" data-i18n="guikoboldaisettings">GUI KoboldAI Settings</option>
</select>
<div class="preset_buttons">
<select id="settings_perset" data-preset-manager-for="kobold">
<option value="gui" data-i18n="guikoboldaisettings">GUI KoboldAI Settings</option>
</select>
<i data-preset-manager-update="kobold" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
<i data-preset-manager-new="kobold" class="menu_button fa-solid fa-plus" title="Create new preset" data-i18n="[title]Create new preset"></i>
<i data-preset-manager-import="kobold" class="menu_button fa-solid fa-upload" title="Import preset" data-i18n="[title]Import preset"></i>
<i data-preset-manager-export="kobold" class="menu_button fa-solid fa-download"title="Export preset" data-i18n="[title]Export preset"></i>
<i data-preset-manager-delete="kobold" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
</div>
</div>
<div id="novel_api-presets">
<h3>
@ -148,7 +158,7 @@
<span class="note-link-span">?</span>
</a>
</h3>
<select id="settings_perset_novel">
<select id="settings_perset_novel" data-preset-manager-for="novel">
<option value="gui" data-i18n="default">Default</option>
</select>
</div>
@ -171,8 +181,15 @@
<div id="textgenerationwebui_api-presets">
<h3><span data-i18n="text gen webio(ooba)preset">Text Gen WebUI (ooba) presets</span>
</h3>
<select id="settings_preset_textgenerationwebui">
</select>
<div class="preset_buttons">
<select id="settings_preset_textgenerationwebui" data-preset-manager-for="textgenerationwebui">
</select>
<i data-preset-manager-update="textgenerationwebui" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
<i data-preset-manager-new="textgenerationwebui" class="menu_button fa-solid fa-plus" title="Create new preset" data-i18n="[title]Create new preset"></i>
<i data-preset-manager-import="textgenerationwebui" class="menu_button fa-solid fa-upload" title="Import preset" data-i18n="[title]Import preset"></i>
<i data-preset-manager-export="textgenerationwebui" class="menu_button fa-solid fa-download"title="Export preset" data-i18n="[title]Export preset"></i>
<i data-preset-manager-delete="textgenerationwebui" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
</div>
</div>
<hr>
</div>
@ -528,6 +545,19 @@
<input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" maxlength="100" />
</div>
</div>
<div class="range-block" data-source="openai,claude">
<div class="range-block-title justifyLeft" data-i18n="Proxy Password">
Proxy Password
</div>
<div class="toggle-description justifyLeft">
<span data-i18n="Will be used as a password for the proxy instead of API key.">
Will be used as a password for the proxy instead of API key.<br>
</span>
</div>
<div class="wide100p">
<input id="openai_proxy_password" type="text" class="text_pole" placeholder="" maxlength="200" />
</div>
</div>
<div class="range-block" data-source="openai,claude">
<div class="range-block-title justifyLeft">
<label for="legacy_streaming" class="checkbox_label">
@ -777,6 +807,111 @@
</div>
</div>
<div id="novel_api-settings">
<div class="range-block">
<div class="range-block-title" data-i18n="Top P">
Top P
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="top_p_novel" name="volume" min="0" max="1" step="0.01">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="top_p_novel" id="top_p_counter_novel">
select
</div>
</div>
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="Top A">
Top A
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="top_a_novel" name="volume" min="0" max="1" step="0.01">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="top_a_novel" id="top_a_counter_novel">
select
</div>
</div>
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="Top K">
Top K
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="top_k_novel" name="volume" min="0" max="300" step="1">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="top_k_novel" id="top_k_counter_novel">
select
</div>
</div>
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="Typical P">
Typical P
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="typical_p_novel" name="volume" min="0" max="1" step="0.01">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="typical_p_novel" id="typical_p_counter_novel">
select
</div>
</div>
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="CFG Scale">
CFG Scale
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="cfg_scale_novel" name="volume" min="1" max="3" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="cfg_scale_novel" id="cfg_scale_counter_novel">
select
</div>
</div>
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="Phrase Repetition Penalty">
Phrase Repetition Penalty
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="phrase_rep_pen_novel" name="volume" min="0" max="5" step="1">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="phrase_rep_pen_novel" id="phrase_rep_pen_counter_novel">
select
</div>
</div>
</div>
</div>
<div class="range-block">
<div class="range-block-title" data-i18n="Min Length">
Min Length
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="min_length_novel" name="volume" min="0" max="1024" step="1">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="min_length_novel" id="min_length_counter_novel">
select
</div>
</div>
</div>
</div>
</div>
<div id="textgenerationwebui_api-settings">
<div class="range-block">
@ -1387,6 +1522,7 @@
<option value="euterpe-v2">Euterpe</option>
<option value="krake-v2">Krake</option>
<option value="clio-v1">Clio</option>
<option value="kayra-v1">Kayra</option>
</select>
</form>
<div id="online_status3">
@ -1458,6 +1594,11 @@
<input id="api_key_openai" name="api_key_openai" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
<div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_openai"></div>
</div>
<div id="ReverseProxyWarningMessage2" class="reverse_proxy_warning">
<b data-i18n="Use Proxy password field instead. This input will be ignored.">
Use "Proxy password" field instead. This input will be ignored.
</b>
</div>
<div data-for="api_key_openai" class="neutral_warning">
For privacy reasons, your API key will be hidden after you reload the page.
</div>
@ -1775,8 +1916,8 @@
<option value="1">GPT-3 (OpenAI)</option>
<option value="2">GPT-3 (Alternative / Classic)</option>
<option value="3">Sentencepiece (LLaMA)</option>
<option value="4">NerdStash (NovelAI Krake)</option>
<option value="5">NerdStash v2 (NovelAI Clio)</option>
<option value="4">NerdStash (NovelAI Clio)</option>
<option value="5">NerdStash v2 (NovelAI Kayra)</option>
<option value="6">API (WebUI)</option>
</select>
</div>
@ -2280,6 +2421,10 @@
<input id="swipes-checkbox" type="checkbox" />
<span data-i18n="Swipes">Swipes</span>
</label>
<label for="fuzzy_search_checkbox">
<input id="fuzzy_search_checkbox" type="checkbox" />
<span data-i18n="Advanced Character Search">Advanced Character Search</span>
</label>
<label for="prefer_character_prompt" title="If checked and the character card contains a prompt override (System Prompt), use that instead." data-i18n="[title]If checked and the character card contains a prompt override (System Prompt), use that instead." class="checkbox_label">
<input id="prefer_character_prompt" type="checkbox" />
<span data-i18n="Prefer Character Card Prompt">Prefer Char. Prompt</span>
@ -2340,7 +2485,7 @@
<span data-i18n="Confirm message deletion">Confirm message deletion</span>
</label>
<label for="spoiler_free_mode"><input id="spoiler_free_mode" type="checkbox" />
<span data-i18n="Spoiler Free Mode">Spolier Free Mode</span>
<span data-i18n="Spoiler Free Mode">Spoiler Free Mode</span>
</label>
<div class="inline-drawer wide100p flexFlowColumn">
@ -2780,6 +2925,8 @@
<option data-field="date_last_chat" data-order="desc" data-i18n="Recent">Recent</option>
<option data-field="chat_size" data-order="desc" data-i18n="Most chats">Most chats</option>
<option data-field="chat_size" data-order="asc" data-i18n="Least chats">Least chats</option>
<option data-field="data_size" data-order="desc" data-i18n="Most tokens">Most tokens</option>
<option data-field="data_size" data-order="asc" data-i18n="Least tokens">Least tokens</option>
<option data-field="name" data-order="random" data-i18n="Random">Random</option>
</select>
</form>
@ -3300,7 +3447,8 @@
<div class="flex-container wide100pLess70px character_select_container">
<div class="wide100p character_name_block">
<span class="ch_name"></span>
<i class="ch_avatar_url"></i>
<small class="character_version"></small>
<small class="ch_avatar_url"></small>
</div>
<i class="ch_fav_icon fa-solid fa-star"></i>
<input class="ch_fav" value="" hidden />

View File

@ -1,6 +1,6 @@
{
"name": "Llama 2",
"system_prompt": "Write {{user}}'s next reply in this fictional roleplay with {{char}}.\n<</SYS>>\n",
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.\n<</SYS>>\n",
"system_sequence": "[INST] <<SYS>>\n",
"stop_sequence": "",
"input_sequence": "[INST]",

View File

@ -76,6 +76,8 @@ import {
persona_description_positions,
loadMovingUIState,
getCustomStoppingStrings,
fuzzySearchCharacters,
MAX_CONTEXT_DEFAULT,
} from "./scripts/power-user.js";
import {
@ -701,11 +703,11 @@ var is_use_scroll_holder = false;
//settings
var settings;
var koboldai_settings;
var koboldai_setting_names;
export let koboldai_settings;
export let koboldai_setting_names;
var preset_settings = "gui";
var user_avatar = "you.png";
var amount_gen = 80; //default max length of AI generated responses
export var amount_gen = 80; //default max length of AI generated responses
var max_context = 2048;
var is_pygmalion = false;
@ -719,8 +721,8 @@ let extension_prompts = {};
var main_api;// = "kobold";
//novel settings
let novel_tier;
let novelai_settings;
let novelai_setting_names;
export let novelai_settings;
export let novelai_setting_names;
let abortController;
//css
@ -895,6 +897,14 @@ async function printCharacters() {
template.find('.ch_description').hide();
}
const version = item.data?.character_version || '';
if (version) {
template.find('.character_version').text(version);
}
else {
template.find('.character_version').hide();
}
// Display inline tags
const tags = getTagsList(item.avatar);
const tagsElement = template.find('.tags');
@ -1224,6 +1234,9 @@ function messageFormatting(mes, ch_name, isSystem, isUser) {
//console.log('mes after removed <tags>')
//console.log(mes)
} */
mes = DOMPurify.sanitize(mes);
return mes;
}
@ -1631,6 +1644,10 @@ function getStoppingStrings(isImpersonate, addSpace) {
result.push(userString);
if (!is_pygmalion && result.includes(youString)) {
result.splice(result.indexOf(youString), 1);
}
// Add other group members as the stopping strings
if (selected_group) {
const group = groups.find(x => x.id === selected_group);
@ -2110,7 +2127,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
// OpenAI doesn't need instruct mode. Use OAI main prompt instead.
const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
const isImpersonate = type == "impersonate";
const isContinue = type == 'continue';
message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
// Name for the multigen prefix
@ -2199,6 +2215,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
type = 'continue';
}
const isContinue = type == 'continue';
deactivateSendButtons();
let { messageBias, promptBias, isUserPromptBias } = getBiasStrings(textareaText, type);
@ -2954,7 +2971,7 @@ function getNextMessageId(type) {
}
export function getBiasStrings(textareaText, type) {
if (type == 'impersonate') {
if (type == 'impersonate' || type == 'continue') {
return { messageBias: '', promptBias: '', isUserPromptBias: false };
}
@ -3047,9 +3064,9 @@ function getMaxContextSize() {
// Should be used with nerdstash tokenizer for best results
this_max_context = Math.min(max_context, 2048);
}
if (nai_settings.model_novel == 'clio-v1') {
// Clio has a max context of 8192
// Should be used with nerdstash_v2 tokenizer for best results
if (nai_settings.model_novel == 'clio-v1' || nai_settings.model_novel == 'kayra-v1') {
// Clio and Kayra has a max context of 8192
// Should be used with nerdstash / nerdstash_v2 tokenizer for best results
this_max_context = Math.min(max_context, 8192);
}
}
@ -3151,6 +3168,17 @@ async function DupeChar() {
return;
}
const confirm = await callPopup(`
<h3>Are you sure you want to duplicate this character?</h3>
<span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span><br><br>`,
'confirm',
);
if (!confirm) {
console.log('User cancelled duplication');
return;
}
const body = { avatar_url: characters[this_chid].avatar };
const response = await fetch('/dupecharacter', {
method: 'POST',
@ -4907,11 +4935,6 @@ async function getSettings(type) {
novelai_setting_names = {};
novelai_setting_names = arr_holder;
nai_settings.preset_settings_novel = settings.preset_settings_novel;
$(
`#settings_perset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]`
).attr("selected", "true");
//Load AI model config settings
amount_gen = settings.amount_gen;
@ -4924,10 +4947,11 @@ async function getSettings(type) {
showSwipeButtons();
// Kobold
loadKoboldSettings(settings);
loadKoboldSettings(settings.kai_settings ?? settings);
// Novel
loadNovelSettings(settings);
loadNovelSettings(settings.nai_settings ?? settings);
$(`#settings_perset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]`).attr("selected", "true");
// TextGen
loadTextGenSettings(data, settings);
@ -5047,8 +5071,8 @@ async function saveSettings(type) {
context_settings: context_settings,
tags: tags,
tag_map: tag_map,
...nai_settings,
...kai_settings,
nai_settings: nai_settings,
kai_settings: kai_settings,
...oai_settings,
}, null, 4),
beforeSend: function () {
@ -5080,7 +5104,23 @@ async function saveSettings(type) {
});
}
export function setGenerationParamsFromPreset(preset) {
if (preset.genamt !== undefined) {
amount_gen = preset.genamt;
$("#amount_gen").val(amount_gen);
$("#amount_gen_counter").text(`${amount_gen}`);
}
if (preset.max_length !== undefined) {
max_context = preset.max_length;
const needsUnlock = max_context > MAX_CONTEXT_DEFAULT;
$('#max_context_unlocked').prop('checked', needsUnlock).trigger('change');
$("#max_context").val(max_context);
$("#max_context_counter").text(`${max_context}`);
}
}
function setCharacterBlockHeight() {
const $children = $("#rm_print_characters_block").children();
@ -5591,11 +5631,19 @@ function onScenarioOverrideRemoveClick() {
$(this).closest('.scenario_override').find('.chat_scenario').val('').trigger('input');
}
function callPopup(text, type, inputValue = '', { okButton, rows } = {}) {
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large } = {}) {
if (type) {
popup_type = type;
}
if (wide) {
$("#dialogue_popup").addClass("wide_dialogue_popup");
}
if (large) {
$("#dialogue_popup").addClass("large_dialogue_popup");
}
$("#dialogue_popup_cancel").css("display", "inline-block");
switch (popup_type) {
case "avatarToCrop":
@ -6704,6 +6752,11 @@ function connectAPISlash(_, text) {
source: 'windowai',
button: '#api_button_openai',
},
'openrouter': {
selected: 'openai',
source: 'openrouter',
button: '#api_button_openai',
},
};
const apiConfig = apiMap[text];
@ -6712,11 +6765,11 @@ function connectAPISlash(_, text) {
return;
}
$("#main_api option[value='" + (apiConfig.selected || text) + "']").prop("selected", true);
$(`#main_api option[value='${apiConfig.selected || text}']`).prop("selected", true);
$("#main_api").trigger('change');
if (apiConfig.source) {
$("#chat_completion_source option[value='" + apiConfig.source + "']").prop("selected", true);
$(`#chat_completion_source option[value='${apiConfig.source}']`).prop("selected", true);
$("#chat_completion_source").trigger('change');
}
@ -6840,6 +6893,73 @@ function doCharListDisplaySwitch() {
updateVisibleDivs('#rm_print_characters_block', true);
}
/**
* Function to handle the deletion of a character, given a specific popup type and character ID.
* If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.
* It fetches the delete character route, sending necessary parameters, and in case of success,
* it proceeds to delete character from UI and saves settings.
* In case of error during the fetch request, it logs the error details.
*
* @param {string} popup_type - The type of popup currently active.
* @param {string} this_chid - The character ID to be deleted.
*/
export async function handleDeleteCharacter(popup_type, this_chid) {
if (popup_type !== "del_ch") {
return;
}
const delete_chats = !!$("#del_char_checkbox").prop("checked");
const avatar = characters[this_chid].avatar;
const name = characters[this_chid].name;
const msg = { avatar_url: avatar, delete_chats: delete_chats };
const response = await fetch('/deletecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(msg),
cache: 'no-cache',
});
if (response.ok) {
await deleteCharacter(name, avatar);
} else {
console.error('Failed to delete character: ', response.status, response.statusText);
}
}
/**
* Function to delete a character from UI after character deletion API success.
* It manages necessary UI changes such as closing advanced editing popup, unsetting
* character ID, resetting characters array and chat metadata, deselecting character's tab
* panel, removing character name from navigation tabs, clearing chat, removing character's
* avatar from tag_map, fetching updated list of characters and updating the 'deleted
* character' message.
* It also ensures to save the settings after all the operations.
*
* @param {string} name - The name of the character to be deleted.
* @param {string} avatar - The avatar URL of the character to be deleted.
*/
export async function deleteCharacter(name, avatar) {
$("#character_cross").click();
this_chid = "invalid-safety-id";
characters.length = 0;
name2 = systemUserName;
chat = [...safetychat];
chat_metadata = {};
setRightTabSelectedClass();
$(document.getElementById("rm_button_selected_ch")).children("h2").text("");
clearChat();
this_chid = undefined;
delete tag_map[avatar];
await getCharacters();
select_rm_info("char_delete", name);
printMessages();
saveSettingsDebounced();
}
$(document).ready(function () {
if (isMobile() === true) {
@ -6918,18 +7038,26 @@ $(document).ready(function () {
$("#character_search_bar").on("input", function () {
const selector = ['#rm_print_characters_block .character_select', '#rm_print_characters_block .group_select'].join(',');
const searchValue = $(this).val().trim().toLowerCase();
const fuzzySearchResults = power_user.fuzzy_search ? fuzzySearchCharacters(searchValue) : [];
function getIsValidSearch(_this) {
const name = $(_this).find(".ch_name").text().toLowerCase();
const chid = $(_this).attr("chid");
if (power_user.fuzzy_search) {
return fuzzySearchResults.includes(parseInt(chid));
}
else {
return name.includes(searchValue);
}
}
if (!searchValue) {
$(selector).removeClass('hiddenBySearch');
updateVisibleDivs('#rm_print_characters_block', true);
} else {
$(selector).each(function () {
const isValidSearch = $(this)
.find(".ch_name")
.text()
.toLowerCase()
.includes(searchValue);
const isValidSearch = getIsValidSearch(this);
$(this).toggleClass('hiddenBySearch', !isValidSearch);
});
updateVisibleDivs('#rm_print_characters_block', true);
@ -7115,7 +7243,7 @@ $(document).ready(function () {
const data = { old_bg, new_bg };
const response = await fetch('/renamebackground', {
method: 'POST',
headers:getRequestHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify(data),
cache: 'no-cache',
});
@ -7211,55 +7339,7 @@ $(document).ready(function () {
}, 200);
}
if (popup_type == "del_ch") {
console.log(
"Deleting character -- ChID: " +
this_chid +
" -- Name: " +
characters[this_chid].name
);
const delete_chats = !!$("#del_char_checkbox").prop("checked");
const avatar = characters[this_chid].avatar;
const name = characters[this_chid].name;
const msg = new FormData($("#form_create").get(0)); // ID form
msg.append("delete_chats", delete_chats);
jQuery.ajax({
method: "POST",
url: "/deletecharacter",
beforeSend: function () {
},
data: msg,
cache: false,
contentType: false,
processData: false,
success: async function (html) {
//RossAscends: New handling of character deletion that avoids page refreshes and should have
// fixed char corruption due to cache problems.
//due to how it is handled with 'popup_type', i couldn't find a way to make my method completely
// modular, so keeping it in TAI-main.js as a new default.
//this allows for dynamic refresh of character list after deleting a character.
// closes advanced editing popup
$("#character_cross").click();
// unsets expected chid before reloading (related to getCharacters/printCharacters from using old arrays)
this_chid = "invalid-safety-id";
// resets the characters array, forcing getcharacters to reset
characters.length = 0;
name2 = systemUserName; // replaces deleted charcter name with system user since she will be displayed next.
chat = [...safetychat]; // sets up system user to tell user about having deleted a character
chat_metadata = {}; // resets chat metadata
setRightTabSelectedClass() // 'deselects' character's tab panel
$(document.getElementById("rm_button_selected_ch"))
.children("h2")
.text(""); // removes character name from nav tabs
clearChat(); // removes deleted char's chat
this_chid = undefined; // prevents getCharacters from trying to load an invalid char.
delete tag_map[avatar]; // removes deleted char's avatar from tag_map
await getCharacters(); // gets the new list of characters (that doesn't include the deleted one)
select_rm_info("char_delete", name); // also updates the 'deleted character' message
printMessages(); // prints out system user's 'deleted character' message
//console.log("#dialogue_popup_ok(del-char) >>>> saving");
saveSettingsDebounced(); // saving settings to keep changes to variables
},
});
handleDeleteCharacter(popup_type, this_chid, characters);
}
if (popup_type == "alternate_greeting" && menu_type !== "create") {
createOrEditCharacter();
@ -7709,13 +7789,7 @@ $(document).ready(function () {
const preset = koboldai_settings[koboldai_setting_names[preset_settings]];
loadKoboldSettings(preset);
amount_gen = preset.genamt;
$("#amount_gen").val(amount_gen);
$("#amount_gen_counter").text(`${amount_gen}`);
max_context = preset.max_length;
$("#max_context").val(max_context);
$("#max_context_counter").text(`${max_context}`);
setGenerationParamsFromPreset(preset);
$("#range_block").find('input').prop("disabled", false);
$("#kobold-advanced-config").find('input').prop("disabled", false);
@ -7908,7 +7982,7 @@ $(document).ready(function () {
.append(
`<textarea id='curEditTextarea' class='edit_textarea' style='max-width:auto;'></textarea>`
);
$('#curEditTextarea').val(text);
$('#curEditTextarea').val(text);
let edit_textarea = $(this)
.closest(".mes_block")
.find(".edit_textarea");
@ -8240,17 +8314,7 @@ $(document).ready(function () {
});
$("#dupe_button").click(async function () {
const body = { avatar_url: characters[this_chid].avatar };
const response = await fetch('/dupecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
});
if (response.ok) {
toastr.success("Character Duplicated");
getCharacters();
}
await DupeChar();
});
$(document).on("click", ".select_chat_block, .bookmark_link, .mes_bookmark", async function () {

View File

@ -239,7 +239,6 @@ $("#rm_ch_create_block").on("input", function () { countTokensDebounced(); });
$("#character_popup").on("input", function () { countTokensDebounced(); });
//function:
export function RA_CountCharTokens() {
$("#result_info").html("");
//console.log('RA_TC -- starting with this_chid = ' + this_chid);
if (menu_type === "create") { //if new char
function saveFormVariables() {
@ -448,8 +447,8 @@ function RA_autoconnect(PrevApi) {
}
break;
case 'openai':
if ((secret_state[SECRET_KEYS.OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)
|| (secret_state[SECRET_KEYS.CLAUDE] && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE)
if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)
|| ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE)
|| (secret_state[SECRET_KEYS.SCALE] && oai_settings.chat_completion_source == chat_completion_sources.SCALE)
|| (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
|| (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)

View File

@ -60,7 +60,10 @@ const extension_settings = {
dice: {},
regex: [],
tts: {},
sd: {},
sd: {
prompts: {},
character_prompts: {},
},
chromadb: {},
translate: {},
objective: {},
@ -70,6 +73,7 @@ const extension_settings = {
fluctuation: 0.1,
enabled: false,
},
speech_recognition: {},
};
let modules = [];

View File

@ -551,10 +551,10 @@ async function onSelectInjectFile(e) {
function doAutoAdjust(chat, maxContext) {
console.debug('CHROMADB: Auto-adjusting sliders (messages: %o, maxContext: %o)', chat.length, maxContext);
// Get mean message length
const meanMessageLength = chat.reduce((acc, cur) => acc + cur.mes.length, 0) / chat.length;
const meanMessageLength = chat.reduce((acc, cur) => acc + (cur?.mes?.length ?? 0), 0) / chat.length;
if (Number.isNaN(meanMessageLength)) {
console.debug('CHROMADB: Mean message length is NaN, aborting auto-adjust');
if (Number.isNaN(meanMessageLength) || meanMessageLength === 0) {
console.debug('CHROMADB: Mean message length is zero or NaN, aborting auto-adjust');
return;
}
@ -611,7 +611,7 @@ window.chromadb_interceptGeneration = async (chat, maxContext) => {
console.debug("CHROMADB: Messages to store length vs keep context: %o vs %o", messagesToStore.length, extension_settings.chromadb.keep_context);
await addMessages(currentChatId, messagesToStore);
}
const lastMessage = chat[chat.length - 1];
let queriedMessages;
@ -811,15 +811,15 @@ jQuery(async () => {
<textarea id="chromadb_custom_msg" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_msg}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_custom_depth" hidden><small>How deep should the memory messages be injected?: (<span id="chromadb_custom_depth_value"></span>)</small></label>
<input id="chromadb_custom_depth" type="range" min="${defaultSettings.chroma_depth_min}" max="${defaultSettings.chroma_depth_max}" step="${defaultSettings.chroma_depth_step}" value="${defaultSettings.chroma_depth}" hidden/>
<label for="chromadb_hhaa_wrapperfmt" hidden><small>Custom wrapper format:</small></label>
<textarea id="chromadb_hhaa_wrapperfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_wrapper}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_hhaa_memoryfmt" hidden><small>Custom memory format:</small></label>
<textarea id="chromadb_hhaa_memoryfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_memory}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_hhaa_token_limit" hidden><small>Maximum tokens allowed for memories: (<span id="chromadb_hhaa_token_limit_value"></span>)</small></label>
<input id="chromadb_hhaa_token_limit" type="range" min="0" max="2048" step="64" value="${defaultSettings.hhaa_token_limit}" hidden/>
<span>Memory Recall Strategy</span>
<select id="chromadb_recall_strategy">
<option value="original">Recall only from this chat</option>
@ -834,7 +834,7 @@ jQuery(async () => {
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
<label for="chromadb_keep_context_proportion"><small>Keep (<span id="chromadb_keep_context_proportion_value"></span>%) of in-context chat messages; replace the rest with memories</small></label>
<input id="chromadb_keep_context_proportion" type="range" min="${defaultSettings.keep_context_proportion_min}" max="${defaultSettings.keep_context_proportion_max}" step="${defaultSettings.keep_context_proportion_step}" value="${defaultSettings.keep_context_proportion}" />
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>

View File

@ -1,4 +1,4 @@
import { chat_metadata } from "../../../script.js";
import { chat_metadata, callPopup, saveSettingsDebounced, getCurrentChatId } from "../../../script.js";
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
import {
substituteParams,
@ -11,92 +11,85 @@ import { registerSlashCommand } from "../../slash-commands.js";
const MODULE_NAME = "Objective"
let globalObjective = ""
let taskTree = null
let globalTasks = []
let currentChatId = ""
let currentObjective = null
let currentTask = null
let checkCounter = 0
const objectivePrompts = {
const defaultPrompts = {
"createTask": `Pause your roleplay and generate a list of tasks to complete an objective. Your next response must be formatted as a numbered list of plain text entries. Do not include anything but the numbered list. The list must be prioritized in the order that tasks must be completed.
The objective that you must make a numbered task list for is: [{{objective}}].
The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Be sure to include the objective as the final task.
The objective that you must make a numbered task list for is: [{{objective}}].
The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Be sure to include the objective as the final task.
Given an example objective of 'Make me a four course dinner', here is an example output:
1. Determine what the courses will be
2. Find recipes for each course
3. Go shopping for supplies with {{user}}
4. Cook the food
5. Get {{user}} to set the table
6. Serve the food
7. Enjoy eating the meal with {{user}}
Given an example objective of 'Make me a four course dinner', here is an example output:
1. Determine what the courses will be
2. Find recipes for each course
3. Go shopping for supplies with {{user}}
4. Cook the food
5. Get {{user}} to set the table
6. Serve the food
7. Enjoy eating the meal with {{user}}
`,
"checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}].
To do this, examine the most recent messages. Your response must only contain either true or false, nothing other words.
Example output:
true
`
To do this, examine the most recent messages. Your response must only contain either true or false, nothing other words.
Example output:
true
`,
'currentTask':`Your current task is [{{task}}]. Balance existing roleplay with completing this task.`
}
const extensionPrompt = "Your current task is [{{task}}]. Balance existing roleplay with completing this task."
let objectivePrompts = defaultPrompts
//###############################//
//# Task Management #//
//###############################//
// Accepts optional index. Defaults to adding to end of list.
function addTask(description, index = null) {
index = index != null ? index: index = globalTasks.length
globalTasks.splice(index, 0, new ObjectiveTask(
{description: description}
))
saveState()
}
// Return the task and index or throw an error
function getTaskById(taskId){
if (taskId == null) {
throw `Null task id`
}
const index = globalTasks.findIndex((task) => task.id === taskId);
if (index !== -1) {
return { task: globalTasks[index], index: index };
} else {
throw `Cannot find task with ${taskId}`
}
return getTaskByIdRecurse(taskId, taskTree)
}
function deleteTask(taskId){
const { task, index } = getTaskById(taskId)
globalTasks.splice(index, 1)
setCurrentTask()
updateUiTaskList()
function getTaskByIdRecurse(taskId, task) {
if (task.id == taskId){
return task
}
for (const childTask of task.children) {
const foundTask = getTaskByIdRecurse(taskId, childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
async function generateTasks() {
const prompt = substituteParams(objectivePrompts["createTask"].replace(/{{objective}}/gi, globalObjective));
const prompt = substituteParams(objectivePrompts.createTask.replace(/{{objective}}/gi, currentObjective.description));
console.log(`Generating tasks for objective with prompt`)
toastr.info('Generating tasks for objective', 'Please wait...');
const taskResponse = await generateQuietPrompt(prompt)
// Clear all existing global tasks when generating
globalTasks = []
// Clear all existing objective tasks when generating
currentObjective.children = []
const numberedListPattern = /^\d+\./
// Create tasks from generated task list
for (const task of taskResponse.split('\n').map(x => x.trim())) {
if (task.match(numberedListPattern) != null) {
addTask(task.replace(numberedListPattern,"").trim())
currentObjective.addTask(task.replace(numberedListPattern,"").trim())
}
}
updateUiTaskList()
console.info(`Response for Objective: '${globalObjective}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks.map(v => {return v.toSaveState()}), null, 2)} `)
updateUiTaskList();
setCurrentTask();
console.info(`Response for Objective: '${taskTree.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks.map(v => {return v.toSaveState()}), null, 2)} `)
toastr.success(`Generated ${globalTasks.length} tasks`, 'Done!');
}
@ -108,12 +101,12 @@ async function checkTaskCompleted() {
}
checkCounter = $('#objective-check-frequency').val()
const prompt = substituteParams(objectivePrompts["checkTaskCompleted"].replace(/{{task}}/gi, currentTask.description));
const prompt = substituteParams(objectivePrompts.checkTaskCompleted.replace(/{{task}}/gi, currentTask.description));
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
// Check response if task complete
if (taskResponse.includes("true")) {
console.info(`Character determined task '${JSON.stringify(currentTask.toSaveState())} is completed.`)
console.info(`Character determined task '${currentTask.description} is completed.`)
currentTask.completeTask()
} else if (!(taskResponse.includes("false"))) {
console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`)
@ -122,27 +115,56 @@ async function checkTaskCompleted() {
}
}
function getNextIncompleteTaskRecurse(task){
// Skip tasks with children, as they will have subtasks to determine completeness
if (task.completed === false && task.children.length === 0){
return task
}
for (const childTask of task.children) {
if (childTask.completed === true){ // Don't recurse into completed tasks
continue
}
const foundTask = getNextIncompleteTaskRecurse(childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
// Set a task in extensionPrompt context. Defaults to first incomplete
function setCurrentTask(taskId = null) {
const context = getContext();
// TODO: Should probably null this rather than set empty object
currentTask = {};
// Set current task to either the next incomplete task, or the index
// Find the task, either next incomplete, or by provided taskId
if (taskId === null) {
currentTask = globalTasks.find(task => !task.completed) || {};
currentTask = getNextIncompleteTaskRecurse(taskTree) || {};
} else {
const { _, index } = getTaskById(taskId)
currentTask = globalTasks[index];
currentTask = getTaskById(taskId);
}
// Get the task description and add to extension prompt
// Don't just check for a current task, check if it has data
const description = currentTask.description || null;
// Now update the extension prompt
if (description) {
const extensionPromptText = extensionPrompt.replace(/{{task}}/gi, description);
const extensionPromptText = objectivePrompts.currentTask.replace(/{{task}}/gi, description);
// Remove highlights
$('.objective-task').css({'border-color':'','border-width':''})
// Highlight current task
let highlightTask = currentTask
while (highlightTask.parentId !== ""){
if (highlightTask.descriptionSpan){
highlightTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'});
}
const parent = getTaskById(highlightTask.parentId)
highlightTask = parent
}
// Update the extension prompt
context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 1, $('#objective-chat-depth').val());
console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`);
} else {
@ -153,22 +175,26 @@ function setCurrentTask(taskId = null) {
saveState();
}
let taskIdCounter = 0
function getNextTaskId(){
// Make sure id does not exist
while (globalTasks.find(task => task.id == taskIdCounter) != undefined) {
taskIdCounter += 1
function getHighestTaskIdRecurse(task) {
let nextId = task.id;
for (const childTask of task.children) {
const childId = getHighestTaskIdRecurse(childTask);
if (childId > nextId) {
nextId = childId;
}
}
const nextId = taskIdCounter
console.log(`TaskID assigned: ${nextId}`)
taskIdCounter += 1
return nextId
return nextId;
}
//###############################//
//# Task Class #//
//###############################//
class ObjectiveTask {
id
description
completed
parent
parentId
children
// UI Elements
@ -178,25 +204,67 @@ class ObjectiveTask {
deleteTaskButton
addTaskButton
constructor ({id=undefined, description, completed=false, parent=null}) {
constructor ({id=undefined, description, completed=false, parentId=""}) {
this.description = description
this.parent = parent
this.parentId = parentId
this.children = []
this.completed = completed
// Generate a new ID if none specified
if (id==undefined){
this.id = getNextTaskId()
this.id = getHighestTaskIdRecurse(taskTree) + 1
} else {
this.id=id
}
}
// Accepts optional index. Defaults to adding to end of list.
addTask(description, index = null) {
index = index != null ? index: index = this.children.length
this.children.splice(index, 0, new ObjectiveTask(
{description: description, parentId: this.id}
))
saveState()
}
getIndex(){
if (this.parentId !== null) {
const parent = getTaskById(this.parentId)
const index = parent.children.findIndex(task => task.id === this.id)
if (index === -1){
throw `getIndex failed: Task '${this.description}' not found in parent task '${parent.description}'`
}
return index
} else {
throw `getIndex failed: Task '${this.description}' has no parent`
}
}
// Used to set parent to complete when all child tasks are completed
checkParentComplete() {
let all_completed = true;
if (this.parentId !== ""){
const parent = getTaskById(this.parentId);
for (const child of parent.children){
if (!child.completed){
all_completed = false;
break;
}
}
if (all_completed){
parent.completed = true;
console.info(`Parent task '${parent.description}' completed after all child tasks complated.`)
} else {
parent.completed = false;
}
}
}
// Complete the current task, setting next task to next incomplete task
completeTask() {
this.completed = true
console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
this.checkParentComplete()
setCurrentTask()
updateUiTaskList()
}
@ -206,9 +274,10 @@ class ObjectiveTask {
const template = `
<div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
<input id="objective-task-complete-${this.id}" type="checkbox">
<span class="text_pole" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
<span class="text_pole objective-task" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
<div id="objective-task-delete-${this.id}" class="objective-task-button fa-solid fa-xmark fa-2x" title="Delete Task"></div>
<div id="objective-task-add-${this.id}" class="objective-task-button fa-solid fa-plus fa-2x" title="Add Task"></div>
<div id="objective-task-add-branch-${this.id}" class="objective-task-button fa-solid fa-code-fork fa-2x" title="Branch Task"></div>
</div><br>
`;
@ -219,6 +288,15 @@ class ObjectiveTask {
this.descriptionSpan = $(`#objective-task-description-${this.id}`);
this.addButton = $(`#objective-task-add-${this.id}`);
this.deleteButton = $(`#objective-task-delete-${this.id}`);
this.taskHtml = $(`#objective-task-label-${this.id}`);
this.branchButton = $(`#objective-task-add-branch-${this.id}`)
// Handle sub-task forking style
if (this.children.length > 0){
this.branchButton.css({'color':'#33cc33'})
} else {
this.branchButton.css({'color':''})
}
// Add event listeners and set properties
$(`#objective-task-complete-${this.id}`).prop('checked', this.completed);
@ -227,52 +305,182 @@ class ObjectiveTask {
$(`#objective-task-description-${this.id}`).on('focusout', () => (this.onDescriptionFocusout()));
$(`#objective-task-delete-${this.id}`).on('click', () => (this.onDeleteClick()));
$(`#objective-task-add-${this.id}`).on('click', () => (this.onAddClick()));
this.branchButton.on('click', () => (this.onBranchClick()))
}
onBranchClick() {
currentObjective = this
updateUiTaskList();
setCurrentTask();
}
onCompleteClick(){
this.completed = this.completedCheckbox.prop('checked')
this.checkParentComplete()
setCurrentTask();
}
onDescriptionUpdate(){
this.description = this.descriptionSpan.text();
}
onDescriptionFocusout(){
setCurrentTask();
}
onDeleteClick(){
deleteTask(this.id);
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.children.splice(index, 1)
updateUiTaskList()
setCurrentTask()
}
onAddClick(){
const {_, index} = getTaskById(this.id)
addTask("", index + 1);
setCurrentTask();
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.addTask("", index + 1);
updateUiTaskList();
setCurrentTask();
}
toSaveState() {
toSaveStateRecurse() {
let children = []
if (this.children.length > 0){
for (const child of this.children){
children.push(child.toSaveStateRecurse())
}
}
return {
"id":this.id,
"description":this.description,
"completed":this.completed,
"parent": this.parent,
"parentId": this.parentId,
"children": children,
}
}
}
//###############################//
//# Custom Prompts #//
//###############################//
function onEditPromptClick() {
let popupText = ''
popupText += `
<div class="objective_prompt_modal">
<div>
<label for="objective-prompt-generate">Generation Prompt</label>
<textarea id="objective-prompt-generate" type="text" class="text_pole textarea_compact" rows="8"></textarea>
<label for="objective-prompt-check">Completion Check Prompt</label>
<textarea id="objective-prompt-check" type="text" class="text_pole textarea_compact" rows="8"></textarea>
<label for="objective-prompt-extension-prompt">Injected Prompt</label>
<textarea id="objective-prompt-extension-prompt" type="text" class="text_pole textarea_compact" rows="8"></textarea>
</div>
<div class="objective_prompt_block">
<input id="objective-custom-prompt-name" style="flex-grow:2" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" placeholder="Custom Prompt Name">
<input id="objective-custom-prompt-save" style="flex-grow:1" class="menu_button" type="submit" value="Save Prompt" />
</div>
<div class="objective_prompt_block">
<label for="objective-prompt-load">Load Prompt</label>
<select id="objective-prompt-load"><select>
<input id="objective-custom-prompt-delete" class="menu_button" type="submit" value="Delete Prompt" />
</div>
</div>`
callPopup(popupText, 'text')
populateCustomPrompts()
// Set current values
$('#objective-prompt-generate').val(objectivePrompts.createTask)
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
// Handle value updates
$('#objective-prompt-generate').on('input', () => {
objectivePrompts.createTask = $('#objective-prompt-generate').val()
})
$('#objective-prompt-check').on('input', () => {
objectivePrompts.checkTaskCompleted = $('#objective-prompt-check').val()
})
$('#objective-prompt-extension-prompt').on('input', () => {
objectivePrompts.currentTask = $('#objective-prompt-extension-prompt').val()
})
// Handle save
$('#objective-custom-prompt-save').on('click', () => {
saveCustomPrompt($('#objective-custom-prompt-name').val(), objectivePrompts)
})
// Handle delete
$('#objective-custom-prompt-delete').on('click', () => {
const optionSelected = $("#objective-prompt-load").find(':selected').val()
deleteCustomPrompt(optionSelected)
})
// Handle load
$('#objective-prompt-load').on('change', loadCustomPrompt)
}
function saveCustomPrompt(customPromptName, customPrompts) {
if (customPromptName == "") {
toastr.warning("Please set custom prompt name to save.")
return
}
if (customPromptName == "default"){
toastr.error("Cannot save over default prompt")
return
}
extension_settings.objective.customPrompts[customPromptName] = {}
Object.assign(extension_settings.objective.customPrompts[customPromptName], customPrompts)
saveSettingsDebounced()
populateCustomPrompts()
}
function deleteCustomPrompt(customPromptName){
if (customPromptName == "default"){
toastr.error("Cannot delete default prompt")
return
}
delete extension_settings.objective.customPrompts[customPromptName]
saveSettingsDebounced()
populateCustomPrompts()
loadCustomPrompt()
}
function loadCustomPrompt(){
const optionSelected = $("#objective-prompt-load").find(':selected').val()
console.log(optionSelected)
Object.assign(objectivePrompts, extension_settings.objective.customPrompts[optionSelected])
$('#objective-prompt-generate').val(objectivePrompts.createTask)
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
}
function populateCustomPrompts(){
// Populate saved prompts
$('#objective-prompt-load').empty()
for (const customPromptName in extension_settings.objective.customPrompts){
const option = document.createElement('option');
option.innerText = customPromptName;
option.value = customPromptName;
option.selected = customPromptName
$('#objective-prompt-load').append(option)
}
}
//###############################//
//# UI AND Settings #//
//###############################//
const defaultSettings = {
objective: "",
tasks: [],
currentObjectiveId: null,
taskTree: null,
chatDepth: 2,
checkFrequency: 3,
hideTasks: false
hideTasks: false,
prompts: defaultPrompts,
}
// Convenient single call. Not much at the moment.
@ -288,15 +496,13 @@ function saveState() {
currentChatId = context.chatId
}
// Convert globalTasks for saving
const tasks = globalTasks.map(task => {return task.toSaveState()})
chat_metadata['objective'] = {
objective: globalObjective,
tasks: tasks,
currentObjectiveId: currentObjective.id,
taskTree: taskTree.toSaveStateRecurse(),
checkFrequency: $('#objective-check-frequency').val(),
chatDepth: $('#objective-chat-depth').val(),
hideTasks: $('#objective-hide-tasks').prop('checked'),
prompts: objectivePrompts,
}
saveMetadataDebounced();
@ -305,12 +511,12 @@ function saveState() {
// Dump core state
function debugObjectiveExtension() {
console.log(JSON.stringify({
"currentTask": currentTask.toSaveState(),
"currentChatId": currentChatId,
"checkCounter": checkCounter,
"globalObjective": globalObjective,
"globalTasks": globalTasks.map(v => {return v.toSaveState()}),
"extension_settings": chat_metadata['objective'],
"currentTask": currentTask,
"currentObjective": currentObjective,
"taskTree": taskTree.toSaveStateRecurse(),
"chat_metadata": chat_metadata['objective'],
"extension_settings": extension_settings['objective'],
"prompts": objectivePrompts
}, null, 2))
}
@ -320,9 +526,20 @@ window.debugObjectiveExtension = debugObjectiveExtension
// Populate UI task list
function updateUiTaskList() {
$('#objective-tasks').empty()
// Show tasks if there are any
if (globalTasks.length > 0){
for (const task of globalTasks) {
// Show button to navigate back to parent objective if parent exists
if (currentObjective){
if (currentObjective.parentId !== "") {
$('#objective-parent').show()
} else {
$('#objective-parent').hide()
}
}
$('#objective-text').val(currentObjective.description)
if (currentObjective.children.length > 0){
// Show tasks if there are any to show
for (const task of currentObjective.children) {
task.addUiElement()
}
} else {
@ -331,17 +548,21 @@ function updateUiTaskList() {
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
`)
$("#objective-task-add-first").on('click', () => {
addTask("")
currentObjective.addTask("")
setCurrentTask()
updateUiTaskList()
})
}
}
function onParentClick() {
currentObjective = getTaskById(currentObjective.parentId)
updateUiTaskList()
setCurrentTask()
}
// Trigger creation of new tasks with given objective.
async function onGenerateObjectiveClick() {
globalObjective = $('#objective-text').val()
await generateTasks()
saveState()
}
@ -352,6 +573,11 @@ function onChatDepthInput() {
setCurrentTask() // Ensure extension prompt is updated
}
function onObjectiveTextFocusOut(){
currentObjective.description = $('#objective-text').val()
saveState()
}
// Update how often we check for task completion
function onCheckFrequencyInput() {
checkCounter = $("#objective-check-frequency").val()
@ -364,10 +590,33 @@ function onHideTasksInput() {
saveState()
}
function loadTaskChildrenRecurse(savedTask) {
let tempTaskTree = new ObjectiveTask({
id: savedTask.id,
description: savedTask.description,
completed: savedTask.completed,
parentId: savedTask.parentId,
})
for (const task of savedTask.children){
const childTask = loadTaskChildrenRecurse(task)
tempTaskTree.children.push(childTask)
}
return tempTaskTree
}
function loadSettings() {
// Load/Init settings for chatId
currentChatId = getContext().chatId
// Reset Objectives and Tasks in memory
taskTree = null;
currentObjective = null;
// Init extension settings
if (Object.keys(extension_settings.objective).length === 0) {
Object.assign(extension_settings.objective, { 'customPrompts': {'default':defaultPrompts}})
}
// Bail on home screen
if (currentChatId == undefined) {
return
@ -375,6 +624,7 @@ function loadSettings() {
// Migrate existing settings
if (currentChatId in extension_settings.objective) {
// TODO: Remove this soon
chat_metadata['objective'] = extension_settings.objective[currentChatId];
delete extension_settings.objective[currentChatId];
}
@ -383,26 +633,52 @@ function loadSettings() {
Object.assign(chat_metadata, { objective: defaultSettings });
}
// Update globals
globalObjective = chat_metadata['objective'].objective
globalTasks = chat_metadata['objective'].tasks.map(task => {
return new ObjectiveTask({
id: task.id,
description: task.description,
completed: task.completed,
parent: task.parent,
})
});
// Migrate legacy flat objective to new objectiveTree and currentObjective
if ('objective' in chat_metadata.objective) {
// Create root objective from legacy objective
taskTree = new ObjectiveTask({id:0, description: chat_metadata.objective.objective});
currentObjective = taskTree;
// Populate root objective tree from legacy tasks
if ('tasks' in chat_metadata.objective) {
let idIncrement = 0;
taskTree.children = chat_metadata.objective.tasks.map(task => {
idIncrement += 1;
return new ObjectiveTask({
id: idIncrement,
description: task.description,
completed: task.completed,
parentId: taskTree.id,
})
});
}
saveState();
delete chat_metadata.objective.objective;
delete chat_metadata.objective.tasks;
} else {
// Load Objectives and Tasks (Normal path)
if (chat_metadata.objective.taskTree){
taskTree = loadTaskChildrenRecurse(chat_metadata.objective.taskTree)
}
}
// Make sure there's a root task
if (!taskTree) {
taskTree = new ObjectiveTask({id:0,description:$('#objective-text').val()})
}
currentObjective = taskTree
checkCounter = chat_metadata['objective'].checkFrequency
// Update UI elements
$('#objective-counter').text(checkCounter)
$("#objective-text").text(globalObjective)
$("#objective-text").text(taskTree.description)
updateUiTaskList()
$('#objective-chat-depth').val(chat_metadata['objective'].chatDepth)
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
$('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks)
onHideTasksInput()
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
setCurrentTask()
}
@ -420,35 +696,44 @@ jQuery(() => {
<div class="objective-settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Objective</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
<div class="objective_block flex-container">
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
<b>Objective</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div id="objective-tasks"> </div>
<div class="objective_block margin-bot-10px">
<div class="objective_block objective_block_control flex1 flexFlowColumn">
<label for="objective-chat-depth">Position in Chat</label>
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
<div class="inline-drawer-content">
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
<div class="objective_block flex-container">
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
</div>
<br>
<div class="objective_block objective_block_control flex1">
<label for="objective-check-frequency">Task Check Frequency</label>
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
<small>(0 = disabled)</small>
<div id="objective-parent" class="objective_block flex-container">
<i class="objective-task-button fa-solid fa-circle-left fa-2x" title="Go to Parent"></i>
<small>Go to parent task</small>
</div>
<div id="objective-tasks"> </div>
<div class="objective_block margin-bot-10px">
<div class="objective_block objective_block_control flex1 flexFlowColumn">
<label for="objective-chat-depth">Position in Chat</label>
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</div>
<br>
<div class="objective_block objective_block_control flex1">
<label for="objective-check-frequency">Task Check Frequency</label>
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
<small>(0 = disabled)</small>
</div>
</div>
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
<div class="objective_block flex-container">
<input id="objective_prompt_edit" class="menu_button" type="submit" value="Edit Prompts" />
</div>
<hr class="sysHR">
</div>
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
<hr class="sysHR">
</div>
</div>`;
</div>
`;
addManualTaskCheckUi()
$('#extensions_settings').append(settingsHtml);
@ -456,6 +741,10 @@ jQuery(() => {
$('#objective-chat-depth').on('input', onChatDepthInput)
$("#objective-check-frequency").on('input', onCheckFrequencyInput)
$('#objective-hide-tasks').on('click', onHideTasksInput)
$('#objective_prompt_edit').on('click', onEditPromptClick)
$('#objective-parent').hide()
$('#objective-parent').on('click',onParentClick)
$('#objective-text').on('focusout',onObjectiveTextFocusOut)
loadSettings()
eventSource.on(event_types.CHAT_CHANGED, () => {

View File

@ -10,6 +10,13 @@
flex-wrap: wrap;
}
.objective_prompt_block {
display: flex;
align-items: baseline;
column-gap: 5px;
flex-wrap: wrap;
}
.objective_block_control {
align-items: baseline;
}

View File

@ -5,13 +5,14 @@ import { regex_placement } from "./engine.js";
async function saveRegexScript(regexScript, existingScriptIndex) {
// If not editing
if (existingScriptIndex === -1) {
// Is the script name undefined?
if (!regexScript.scriptName) {
toastr.error(`Could not save regex script: The script name was undefined or empty!`);
return;
}
// Is the script name undefined or empty?
if (!regexScript.scriptName) {
toastr.error(`Could not save regex script: The script name was undefined or empty!`);
return;
}
if (existingScriptIndex === -1) {
// Does the script name already exist?
if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
@ -29,14 +30,12 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
// Is a find regex present?
if (regexScript.findRegex.length === 0) {
toastr.error(`Could not save regex script: A find regex is required!`);
return;
toastr.warning(`This regex script will not work, but was saved anyway: A find regex isn't present.`);
}
// Is there someplace to place results?
if (regexScript.placement.length === 0) {
toastr.error(`Could not save regex script: One placement checkbox must be selected!`);
return;
toastr.warning(`This regex script will not work, but was saved anyway: One "Affects" checkbox must be selected!`);
}
if (existingScriptIndex !== -1) {
@ -140,7 +139,7 @@ async function onRegexEditorOpenClick(existingId) {
.prop("checked", true);
editorHtml
.find(`input[name="replace_position"][value="0"]`)
.find(`input[name="replace_position"][value="1"]`)
.prop("checked", true);
}

View File

@ -0,0 +1,233 @@
// Borrowed from Agnai (AGPLv3)
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
// First version by Cohee#1207
// Adapted by Tony-sama
export { BrowserSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Browser)> "
class BrowserSttProvider {
//########//
// Config //
//########//
settings = {
language: ""
}
defaultSettings = {
language: "en-US",
}
processTranscriptFunction = null;
get settingsHtml() {
let html = ' \
<span>Language</span> </br> \
<select id="speech_recognition_browser_provider_language"> \
<option value="ar-SA">ar-SA: Arabic (Saudi Arabia)</option> \
<option value="bn-BD">bn-BD: Bangla (Bangladesh)</option> \
<option value="bn-IN">bn-IN: Bangla (India)</option> \
<option value="cs-CZ">cs-CZ: Czech (Czech Republic)</option> \
<option value="da-DK">da-DK: Danish (Denmark)</option> \
<option value="de-AT">de-AT: German (Austria)</option> \
<option value="de-CH">de-CH: German (Switzerland)</option> \
<option value="de-DE">de-DE: German (Germany)</option> \
<option value="el-GR">el-GR: Greek (Greece)</option> \
<option value="en-AU">en-AU: English (Australia)</option> \
<option value="en-CA">en-CA: English (Canada)</option> \
<option value="en-GB">en-GB: English (United Kingdom)</option> \
<option value="en-IE">en-IE: English (Ireland)</option> \
<option value="en-IN">en-IN: English (India)</option> \
<option value="en-NZ">en-NZ: English (New Zealand)</option> \
<option value="en-US">en-US: English (United States)</option> \
<option value="en-ZA">en-ZA: English (South Africa)</option> \
<option value="es-AR">es-AR: Spanish (Argentina)</option> \
<option value="es-CL">es-CL: Spanish (Chile)</option> \
<option value="es-CO">es-CO: Spanish (Columbia)</option> \
<option value="es-ES">es-ES: Spanish (Spain)</option> \
<option value="es-MX">es-MX: Spanish (Mexico)</option> \
<option value="es-US">es-US: Spanish (United States)</option> \
<option value="fi-FI">fi-FI: Finnish (Finland)</option> \
<option value="fr-BE">fr-BE: French (Belgium)</option> \
<option value="fr-CA">fr-CA: French (Canada)</option> \
<option value="fr-CH">fr-CH: French (Switzerland)</option> \
<option value="fr-FR">fr-FR: French (France)</option> \
<option value="he-IL">he-IL: Hebrew (Israel)</option> \
<option value="hi-IN">hi-IN: Hindi (India)</option> \
<option value="hu-HU">hu-HU: Hungarian (Hungary)</option> \
<option value="id-ID">id-ID: Indonesian (Indonesia)</option> \
<option value="it-CH">it-CH: Italian (Switzerland)</option> \
<option value="it-IT">it-IT: Italian (Italy)</option> \
<option value="ja-JP">ja-JP: Japanese (Japan)</option> \
<option value="ko-KR">ko-KR: Korean (Republic of Korea)</option> \
<option value="nl-BE">nl-BE: Dutch (Belgium)</option> \
<option value="nl-NL">nl-NL: Dutch (The Netherlands)</option> \
<option value="no-NO">no-NO: Norwegian (Norway)</option> \
<option value="pl-PL">pl-PL: Polish (Poland)</option> \
<option value="pt-BR">pt-BR: Portugese (Brazil)</option> \
<option value="pt-PT">pt-PT: Portugese (Portugal)</option> \
<option value="ro-RO">ro-RO: Romanian (Romania)</option> \
<option value="ru-RU">ru-RU: Russian (Russian Federation)</option> \
<option value="sk-SK">sk-SK: Slovak (Slovakia)</option> \
<option value="sv-SE">sv-SE: Swedish (Sweden)</option> \
<option value="ta-IN">ta-IN: Tamil (India)</option> \
<option value="ta-LK">ta-LK: Tamil (Sri Lanka)</option> \
<option value="th-TH">th-TH: Thai (Thailand)</option> \
<option value="tr-TR">tr-TR: Turkish (Turkey)</option> \
<option value="zh-CN">zh-CN: Chinese (China)</option> \
<option value="zh-HK">zh-HK: Chinese (Hond Kong)</option> \
<option value="zh-TW">zh-TW: Chinese (Taiwan)</option> \
</select> \
'
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.language = $("#speech_recognition_browser_provider_language").val();
console.debug(DEBUG_PREFIX+"Change language to",this.settings.language);
this.loadSettings(this.settings);
}
static capitalizeInterim(interimTranscript) {
let capitalizeIndex = -1;
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
else if (interimTranscript.length > 1) capitalizeIndex = 0;
if (capitalizeIndex > -1) {
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
const rest = interimTranscript.substring(capitalizeIndex + 1);
interimTranscript = spacing + capitalized + rest;
}
return interimTranscript;
}
static composeValues(previous, interim) {
let spacing = '';
if (previous.endsWith('.')) spacing = ' ';
return previous + spacing + interim;
}
loadSettings(settings) {
const processTranscript = this.processTranscriptFunction;
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default browser STT settings")
}
// Initialise as defaultSettings
this.settings = this.defaultSettings;
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to Speech recogniton extension (browser): ${key}`
}
}
$("#speech_recognition_browser_provider_language").val(this.settings.language);
const speechRecognitionSettings = $.extend({
grammar: '' // Custom grammar
}, options);
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const speechRecognitionList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
if (!speechRecognition) {
console.warn(DEBUG_PREFIX+'Speech recognition is not supported in this browser.');
$("#microphone_button").hide();
toastr.error("Speech recognition is not supported in this browser, use another browser or another provider of SillyTavern-extras Speech recognition extension.", "Speech recognition activation Failed (Browser)", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
const recognition = new speechRecognition();
if (speechRecognitionSettings.grammar && speechRecognitionList) {
speechRecognitionList.addFromString(speechRecognitionSettings.grammar, 1);
recognition.grammars = speechRecognitionList;
}
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = this.settings.language;
const textarea = $('#send_textarea');
const button = $('#microphone_button');
let listening = false;
button.off('click').on("click", function () {
if (listening) {
recognition.stop();
} else {
recognition.start();
}
listening = !listening;
});
let initialText = '';
recognition.onresult = function (speechEvent) {
let finalTranscript = '';
let interimTranscript = ''
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
const transcript = speechEvent.results[i][0].transcript;
if (speechEvent.results[i].isFinal) {
let interim = BrowserSttProvider.capitalizeInterim(transcript);
if (interim != '') {
let final = finalTranscript;
final = BrowserSttProvider.composeValues(final, interim);
if (final.slice(-1) != '.' & final.slice(-1) != '?') final += '.';
finalTranscript = final;
recognition.abort();
listening = false;
}
interimTranscript = ' ';
} else {
interimTranscript += transcript;
}
}
interimTranscript = BrowserSttProvider.capitalizeInterim(interimTranscript);
textarea.val(initialText + finalTranscript + interimTranscript);
};
recognition.onerror = function (event) {
console.error('Error occurred in recognition:', event.error);
//if ($('#speech_recognition_debug').is(':checked'))
// toastr.error('Error occurred in recognition:'+ event.error, 'STT Generation error (Browser)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
};
recognition.onend = function () {
listening = false;
button.toggleClass('fa-microphone fa-microphone-slash');
const newText = textarea.val().substring(initialText.length);
textarea.val(textarea.val().substring(0,initialText.length));
processTranscript(newText);
};
recognition.onstart = function () {
initialText = textarea.val();
button.toggleClass('fa-microphone fa-microphone-slash');
if ($("#speech_recognition_message_mode").val() == "replace") {
textarea.val("");
initialText = ""
}
};
$("#microphone_button").show();
console.debug(DEBUG_PREFIX+"Browser STT settings loaded")
}
}

View File

@ -1,110 +1,351 @@
// Borrowed from Agnai (AGPLv3)
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
function capitalizeInterim(interimTranscript) {
let capitalizeIndex = -1;
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
else if (interimTranscript.length > 1) capitalizeIndex = 0;
if (capitalizeIndex > -1) {
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
const rest = interimTranscript.substring(capitalizeIndex + 1);
interimTranscript = spacing + capitalized + rest;
/*
TODO:
- try pseudo streaming audio by just sending chunk every X seconds and asking VOSK if it is full text.
*/
import { saveSettingsDebounced } from "../../../script.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { VoskSttProvider } from './vosk.js'
import { WhisperSttProvider } from './whisper.js'
import { BrowserSttProvider } from './browser.js'
export { MODULE_NAME };
const MODULE_NAME = 'Speech Recognition';
const DEBUG_PREFIX = "<Speech Recognition module> "
let sttProviders = {
None: null,
Browser: BrowserSttProvider,
Whisper: WhisperSttProvider,
Vosk: VoskSttProvider,
}
let sttProvider = null
let sttProviderName = "None"
let audioRecording = false
const constraints = { audio: { sampleSize: 16, channelCount: 1, sampleRate: 16000 } };
let audioChunks = [];
async function processTranscript(transcript) {
try {
const transcriptOriginal = transcript;
let transcriptFormatted = transcriptOriginal.trim();
if (transcriptFormatted.length > 0)
{
console.debug(DEBUG_PREFIX+"recorded transcript: \""+transcriptFormatted+"\"");
const messageMode = extension_settings.speech_recognition.messageMode;
console.debug(DEBUG_PREFIX+"mode: "+messageMode);
let transcriptLower = transcriptFormatted.toLowerCase()
// remove punctuation
let transcriptRaw = transcriptLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
// Check message mapping
if (extension_settings.speech_recognition.messageMappingEnabled) {
console.debug(DEBUG_PREFIX+"Start searching message mapping into:",transcriptRaw)
for (const key in extension_settings.speech_recognition.messageMapping) {
console.debug(DEBUG_PREFIX+"message mapping searching: ", key,"=>",extension_settings.speech_recognition.messageMapping[key]);
if (transcriptRaw.includes(key)) {
var message = extension_settings.speech_recognition.messageMapping[key];
console.debug(DEBUG_PREFIX+"message mapping found: ", key,"=>",extension_settings.speech_recognition.messageMapping[key]);
$("#send_textarea").val(message);
if (messageMode == "auto_send") await getContext().generate();
return;
}
}
}
console.debug(DEBUG_PREFIX+"no message mapping found, processing transcript as normal message");
switch (messageMode) {
case "auto_send":
$('#send_textarea').val("") // clear message area to avoid double message
console.debug(DEBUG_PREFIX+"Sending message")
const context = getContext();
const messageText = transcriptFormatted;
const message = {
name: context.name1,
is_user: true,
is_name: true,
send_date: Date.now(),
mes: messageText,
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate();
$('#debug_output').text("<SST-module DEBUG>: message sent: \""+ transcriptFormatted +"\"");
break;
case "replace":
console.debug(DEBUG_PREFIX+"Replacing message")
$('#send_textarea').val(transcriptFormatted);
break;
case "append":
console.debug(DEBUG_PREFIX+"Appending message")
$('#send_textarea').val($('#send_textarea').val()+" "+transcriptFormatted);
break;
default:
console.debug(DEBUG_PREFIX+"Not supported stt message mode: "+messageMode)
}
}
else
{
console.debug(DEBUG_PREFIX+"Empty transcript, do nothing");
}
}
catch (error) {
console.debug(error);
}
return interimTranscript;
}
function composeValues(previous, interim) {
let spacing = '';
if (previous.endsWith('.')) spacing = ' ';
return previous + spacing + interim;
function loadNavigatorAudioRecording() {
if (navigator.mediaDevices.getUserMedia) {
console.debug(DEBUG_PREFIX+' getUserMedia supported by browser.');
let onSuccess = function(stream) {
const mediaRecorder = new MediaRecorder(stream);
$("#microphone_button").off('click').on("click", function() {
if (!audioRecording) {
mediaRecorder.start();
console.debug(mediaRecorder.state);
console.debug("recorder started");
audioRecording = true;
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
}
else {
mediaRecorder.stop();
console.debug(mediaRecorder.state);
console.debug("recorder stopped");
audioRecording = false;
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
}
});
mediaRecorder.onstop = async function() {
console.debug(DEBUG_PREFIX+"data available after MediaRecorder.stop() called: ", audioChunks.length, " chunks");
const audioBlob = new Blob(audioChunks, { type: "audio/wav; codecs=0" });
audioChunks = [];
const transcript = await sttProvider.processAudio(audioBlob);
// TODO: lock and release recording while processing?
console.debug(DEBUG_PREFIX+"received transcript:", transcript);
processTranscript(transcript);
}
mediaRecorder.ondataavailable = function(e) {
audioChunks.push(e.data);
}
}
let onError = function(err) {
console.debug(DEBUG_PREFIX+"The following error occured: " + err);
}
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
} else {
console.debug(DEBUG_PREFIX+"getUserMedia not supported on your browser!");
toastr.error("getUserMedia not supported", DEBUG_PREFIX+"not supported for your browser.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
}
}
(function ($) {
$.fn.speechRecognitionPlugin = function (options) {
const settings = $.extend({
grammar: '' // Custom grammar
}, options);
//##############//
// STT Provider //
//##############//
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const speechRecognitionList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
function loadSttProvider(provider) {
//Clear the current config and add new config
$("#speech_recognition_provider_settings").html("");
if (!speechRecognition) {
console.warn('Speech recognition is not supported in this browser.');
return;
// Init provider references
extension_settings.speech_recognition.currentProvider = provider;
sttProviderName = provider;
if (!(sttProviderName in extension_settings.speech_recognition)) {
console.warn(`Provider ${sttProviderName} not in Extension Settings, initiatilizing provider in settings`);
extension_settings.speech_recognition[sttProviderName] = {};
}
$('#speech_recognition_provider').val(sttProviderName);
if (sttProviderName == "None") {
$("#microphone_button").hide();
$("#speech_recognition_message_mode_div").hide();
$("#speech_recognition_message_mapping_div").hide();
return;
}
$("#speech_recognition_message_mode_div").show();
$("#speech_recognition_message_mapping_div").show();
sttProvider = new sttProviders[sttProviderName]
// Init provider settings
$('#speech_recognition_provider_settings').append(sttProvider.settingsHtml);
// Use microphone button as push to talk
if (sttProviderName == "Browser") {
sttProvider.processTranscriptFunction = processTranscript;
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
}
else {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
loadNavigatorAudioRecording();
$("#microphone_button").show();
}
}
function onSttProviderChange() {
const sttProviderSelection = $('#speech_recognition_provider').val();
loadSttProvider(sttProviderSelection);
saveSettingsDebounced();
}
function onSttProviderSettingsInput() {
sttProvider.onSettingsChange();
// Persist changes to SillyTavern stt extension settings
extension_settings.speech_recognition[sttProviderName] = sttProvider.settings;
saveSettingsDebounced();
console.info(`Saved settings ${sttProviderName} ${JSON.stringify(sttProvider.settings)}`);
}
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
currentProvider: "None",
messageMode: "append",
messageMappingText: "",
messageMapping: [],
messageMappingEnabled: false
}
function loadSettings() {
if (Object.keys(extension_settings.speech_recognition).length === 0) {
Object.assign(extension_settings.speech_recognition, defaultSettings)
}
$('#speech_recognition_enabled').prop('checked',extension_settings.speech_recognition.enabled);
$('#speech_recognition_message_mode').val(extension_settings.speech_recognition.messageMode);
if (extension_settings.speech_recognition.messageMappingText.length > 0) {
$('#speech_recognition_message_mapping').val(extension_settings.speech_recognition.messageMappingText);
}
$('#speech_recognition_message_mapping_enabled').prop('checked',extension_settings.speech_recognition.messageMappingEnabled);
}
async function onMessageModeChange() {
extension_settings.speech_recognition.messageMode = $('#speech_recognition_message_mode').val();
if(sttProviderName != "Browser" & extension_settings.speech_recognition.messageMode == "auto_send") {
$("#speech_recognition_wait_response_div").show()
}
else {
$("#speech_recognition_wait_response_div").hide()
}
saveSettingsDebounced();
}
async function onMessageMappingChange() {
let array = $('#speech_recognition_message_mapping').val().split(",");
array = array.map(element => {return element.trim();});
array = array.filter((str) => str !== '');
extension_settings.speech_recognition.messageMapping = {};
for (const text of array) {
if (text.includes("=")) {
const pair = text.toLowerCase().split("=")
extension_settings.speech_recognition.messageMapping[pair[0].trim()] = pair[1].trim()
console.debug(DEBUG_PREFIX+"Added mapping", pair[0],"=>", extension_settings.speech_recognition.messageMapping[pair[0]]);
}
const recognition = new speechRecognition();
if (settings.grammar && speechRecognitionList) {
speechRecognitionList.addFromString(settings.grammar, 1);
recognition.grammars = speechRecognitionList;
else {
console.debug(DEBUG_PREFIX+"Wrong syntax for message mapping, no '=' found in:", text);
}
}
$("#speech_recognition_message_mapping_status").text("Message mapping updated to: "+JSON.stringify(extension_settings.speech_recognition.messageMapping))
console.debug(DEBUG_PREFIX+"Updated message mapping", extension_settings.speech_recognition.messageMapping);
extension_settings.speech_recognition.messageMappingText = $('#speech_recognition_message_mapping').val()
saveSettingsDebounced();
}
recognition.continuous = true;
recognition.interimResults = true;
// TODO: This should be configurable.
recognition.lang = 'en-US'; // Set the language to English (US).
async function onMessageMappingEnabledClick() {
extension_settings.speech_recognition.messageMappingEnabled = $('#speech_recognition_message_mapping_enabled').is(':checked');
saveSettingsDebounced()
}
const $textarea = this;
const $button = $('<div class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
$(document).ready(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="speech_recognition_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Speech Recognition</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<span>Select Speech-to-text Provider</span> </br>
<select id="speech_recognition_provider">
</select>
</div>
<div id="speech_recognition_message_mode_div">
<span>Message Mode</span> </br>
<select id="speech_recognition_message_mode">
<option value="append">Append</option>
<option value="replace">Replace</option>
<option value="auto_send">Auto send</option>
</select>
</div>
<div id="speech_recognition_message_mapping_div">
<span>Message Mapping</span>
<textarea id="speech_recognition_message_mapping" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated phrases mapping, example:\ncommand delete = /del 2,\nslash delete = /del 2,\nsystem roll = /roll 2d6,\nhey continue = /continue"></textarea>
<span id="speech_recognition_message_mapping_status"></span>
<label class="checkbox_label" for="speech_recognition_message_mapping_enabled">
<input type="checkbox" id="speech_recognition_message_mapping_enabled" name="speech_recognition_message_mapping_enabled">
<small>Enable messages mapping</small>
</label>
</div>
<form id="speech_recognition_provider_settings" class="inline-drawer-content">
</form>
</div>
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#speech_recognition_provider_settings').on('input', onSttProviderSettingsInput);
for (const provider in sttProviders) {
$('#speech_recognition_provider').append($("<option />").val(provider).text(provider));
console.debug(DEBUG_PREFIX+"added option "+provider);
}
$('#speech_recognition_provider').on('change', onSttProviderChange);
$('#speech_recognition_message_mode').on('change', onMessageModeChange);
$('#speech_recognition_message_mapping').on('change', onMessageMappingChange);
$('#speech_recognition_message_mapping_enabled').on('click', onMessageMappingEnabledClick);
const $button = $('<div id="microphone_button" class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
$('#send_but_sheld').prepend($button);
let listening = false;
$button.on('click', function () {
if (listening) {
recognition.stop();
} else {
recognition.start();
}
listening = !listening;
});
}
addExtensionControls(); // No init dependencies
loadSettings(); // Depends on Extension Controls and loadTtsProvider
loadSttProvider(extension_settings.speech_recognition.currentProvider); // No dependencies
let initialText = '';
recognition.onresult = function (speechEvent) {
let finalTranscript = '';
let interimTranscript = ''
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
const transcript = speechEvent.results[i][0].transcript;
if (speechEvent.results[i].isFinal) {
let interim = capitalizeInterim(transcript);
if (interim != '') {
let final = finalTranscript;
final = composeValues(final, interim) + '.';
finalTranscript = final;
recognition.abort();
listening = false;
}
interimTranscript = ' ';
} else {
interimTranscript += transcript;
}
}
interimTranscript = capitalizeInterim(interimTranscript);
$textarea.val(initialText + finalTranscript + interimTranscript);
};
recognition.onerror = function (event) {
console.error('Error occurred in recognition:', event.error);
};
recognition.onend = function () {
listening = false;
$button.toggleClass('fa-microphone fa-microphone-slash');
};
recognition.onstart = function () {
initialText = $textarea.val();
$button.toggleClass('fa-microphone fa-microphone-slash');
};
};
}(jQuery));
jQuery(() => {
const $textarea = $('#send_textarea');
$textarea.speechRecognitionPlugin();
});
//const wrapper = new ModuleWorkerWrapper(moduleWorker);
//setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
//moduleWorker();
})

View File

@ -2,10 +2,13 @@
"display_name": "Speech Recognition",
"loading_order": 13,
"requires": [],
"optional": [],
"optional": [
"vosk-speech-recognition",
"whisper-speech-recognition"
],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"author": "Cohee#1207 and Keij#6799",
"version": "1.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -0,0 +1,65 @@
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
export { VoskSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Vosk)> "
class VoskSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
}
get settingsHtml() {
let html = ""
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default vosk STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
console.debug(DEBUG_PREFIX+"Vosk STT settings loaded")
}
async processAudio(audioblob) {
var requestData = new FormData();
requestData.append('AudioFile', audioblob, 'record.wav');
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/vosk/process-audio';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, 'STT Generation Failed (Vosk)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const result = await apiResult.json();
return result.transcript;
}
}

View File

@ -0,0 +1,67 @@
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
export { WhisperSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Vosk)> "
class WhisperSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
//model_path: "",
}
get settingsHtml() {
let html = ""
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default Whisper STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
console.debug(DEBUG_PREFIX+"Whisper STT settings loaded")
}
async processAudio(audioblob) {
var requestData = new FormData();
requestData.append('AudioFile', audioblob, 'record.wav');
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/whisper/process-audio';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, 'STT Generation Failed (Whisper)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const result = await apiResult.json();
return result.transcript;
}
}

View File

@ -8,10 +8,13 @@ import {
getRequestHeaders,
event_types,
eventSource,
appendImageToMessage
appendImageToMessage,
generateQuietPrompt,
this_chid,
} from "../../../script.js";
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment } from "../../utils.js";
import { selected_group } from "../../group-chats.js";
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename } from "../../utils.js";
export { MODULE_NAME };
// Wraps a string into monospace font-face span
@ -39,6 +42,15 @@ const generationMode = {
FREE: 6,
}
const modeLabels = {
[generationMode.CHARACTER]: 'Character ("Yourself")',
[generationMode.FACE]: 'Portrait ("Your Face")',
[generationMode.USER]: 'User ("Me")',
[generationMode.SCENARIO]: 'Scenario ("The Whole Story")',
[generationMode.NOW]: 'Last Message',
[generationMode.RAW_LAST]: 'Raw Last Message',
}
const triggerWords = {
[generationMode.CHARACTER]: ['you'],
[generationMode.USER]: ['me'],
@ -48,7 +60,7 @@ const triggerWords = {
[generationMode.FACE]: ['face'],
}
const quietPrompts = {
const promptTemplates = {
/*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */
[generationMode.CHARACTER]: "[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase 'full body portrait,']",
//face-specific prompt
@ -134,6 +146,8 @@ const defaultSettings = {
// Refine mode
refine_mode: false,
prompts: promptTemplates,
}
async function loadSettings() {
@ -141,6 +155,14 @@ async function loadSettings() {
Object.assign(extension_settings.sd, defaultSettings);
}
if (extension_settings.sd.prompts === undefined) {
extension_settings.sd.prompts = promptTemplates;
}
if (extension_settings.sd.character_prompts === undefined) {
extension_settings.sd.character_prompts = {};
}
$('#sd_scale').val(extension_settings.sd.scale).trigger('input');
$('#sd_steps').val(extension_settings.sd.steps).trigger('input');
$('#sd_prompt_prefix').val(extension_settings.sd.prompt_prefix).trigger('input');
@ -154,9 +176,104 @@ async function loadSettings() {
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
addPromptTemplates();
await Promise.all([loadSamplers(), loadModels()]);
}
function addPromptTemplates() {
$('#sd_prompt_templates').empty();
for (const [name, prompt] of Object.entries(extension_settings.sd.prompts)) {
const label = $('<label></label>')
.text(modeLabels[name])
.attr('for', `sd_prompt_${name}`);
const textarea = $('<textarea></textarea>')
.addClass('textarea_compact text_pole')
.attr('id', `sd_prompt_${name}`)
.attr('rows', 6)
.val(prompt).on('input', () => {
extension_settings.sd.prompts[name] = textarea.val();
saveSettingsDebounced();
});
const button = $('<button></button>')
.addClass('menu_button fa-solid fa-undo')
.attr('title', 'Restore default')
.on('click', () => {
textarea.val(promptTemplates[name]);
extension_settings.sd.prompts[name] = promptTemplates[name];
saveSettingsDebounced();
});
const container = $('<div></div>')
.addClass('title_restorable')
.append(label)
.append(button)
$('#sd_prompt_templates').append(container);
$('#sd_prompt_templates').append(textarea);
}
}
async function refinePrompt(prompt) {
if (extension_settings.sd.refine_mode) {
const refinedPrompt = await callPopup('<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.', 'input', prompt, { rows: 5, okButton: 'Generate' });
if (refinedPrompt) {
return refinedPrompt;
} else {
throw new Error('Generation aborted by user.');
}
}
return prompt;
}
function onChatChanged() {
if (this_chid === undefined || selected_group) {
$('#sd_character_prompt_block').hide();
return;
}
$('#sd_character_prompt_block').show();
const key = getCharaFilename(this_chid);
$('#sd_character_prompt').val(key ? (extension_settings.sd.character_prompts[key] || '') : '');
}
function onCharacterPromptInput() {
const key = getCharaFilename(this_chid);
extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
resetScrollHeight($(this));
saveSettingsDebounced();
}
function getCharacterPrefix() {
if (selected_group) {
return '';
}
const key = getCharaFilename(this_chid);
if (key) {
return extension_settings.sd.character_prompts[key] || '';
}
return '';
}
function combinePrefixes(str1, str2) {
if (!str2) {
return str1;
}
// Remove leading/trailing white spaces and commas from the strings
str1 = str1.trim().replace(/^,|,$/g, '');
str2 = str2.trim().replace(/^,|,$/g, '');
// Combine the strings with a comma between them
var result = `${str1}, ${str2},`;
return result;
}
function onRefineModeInput() {
extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
saveSettingsDebounced();
@ -383,7 +500,7 @@ function getQuietPrompt(mode, trigger) {
return trigger;
}
return substituteParams(stringFormat(quietPrompts[mode], trigger));
return substituteParams(stringFormat(extension_settings.sd.prompts[mode], trigger));
}
function processReply(str) {
@ -438,7 +555,8 @@ async function generatePicture(_, trigger, message, callback) {
const prevSDHeight = extension_settings.sd.height;
if (generationType == generationMode.FACE) {
extension_settings.sd.height = extension_settings.sd.width * 1.5;
// Round to nearest multiple of 64
extension_settings.sd.height = Math.round(extension_settings.sd.height * 1.5 / 64) * 64;
}
try {
@ -475,31 +593,16 @@ async function getPrompt(generationType, message, trigger, quiet_prompt) {
break;
}
if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt);
}
return prompt;
}
async function generatePrompt(quiet_prompt) {
let reply = processReply(await new Promise(
async function promptPromise(resolve, reject) {
try {
await getContext().generate('quiet', { resolve, reject, quiet_prompt, force_name2: true, });
}
catch {
reject();
}
}));
if (extension_settings.sd.refine_mode) {
const refinedPrompt = await callPopup('<h3>Review and edit the generated prompt:</h3>Press "Cancel" to abort the image generation.', 'input', reply, { rows: 5, okButton: 'Generate' });
if (refinedPrompt) {
reply = refinedPrompt;
} else {
throw new Error('Generation aborted by user.');
}
}
return reply;
const reply = await generateQuietPrompt(quiet_prompt);
return processReply(reply);
}
async function sendGenerationRequest(prompt, callback) {
@ -524,7 +627,7 @@ async function generateExtrasImage(prompt, callback) {
scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
prompt_prefix: extension_settings.sd.prompt_prefix,
prompt_prefix: combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()),
negative_prompt: extension_settings.sd.negative_prompt,
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
@ -552,7 +655,7 @@ async function generateHordeImage(prompt, callback) {
scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
prompt_prefix: extension_settings.sd.prompt_prefix,
prompt_prefix: combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()),
negative_prompt: extension_settings.sd.negative_prompt,
model: extension_settings.sd.model,
nsfw: extension_settings.sd.horde_nsfw,
@ -690,7 +793,9 @@ async function sdMessageButton(e) {
try {
setBusyIcon(true);
if (hasSavedImage) {
const prompt = message?.extra?.title;
const prompt = await refinePrompt(message.extra.title);
message.extra.title = prompt;
console.log('Regenerating an image, using existing prompt:', prompt);
await sendGenerationRequest(prompt, saveGeneratedImage);
}
@ -763,60 +868,74 @@ jQuery(async () => {
<div class="sd_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Stable Diffusion</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
<b>Stable Diffusion</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><i>Use slash commands or the bottom Paintbrush button to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small>
<br>
<small><i>Hint: Save an API key in Horde KoboldAI API settings to use it here.</i></small>
<label for="sd_refine_mode" class="checkbox_label" title="Allow to edit prompts manually before sending them to generation API">
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
</label>
<div class="flex-container flexGap5 marginTop10 margin-bot-10px">
<label class="checkbox_label">
<input id="sd_horde" type="checkbox" />
Use Stable Horde
</label>
<label style="margin-left:1em;" class="checkbox_label">
<input id="sd_horde_nsfw" type="checkbox" />
Allow NSFW images from Horde
</label>
</div>
<label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="${defaultSettings.scale_min}" max="${defaultSettings.scale_max}" step="${defaultSettings.scale_step}" value="${defaultSettings.scale}" />
<label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="${defaultSettings.steps_min}" max="${defaultSettings.steps_max}" step="${defaultSettings.steps_step}" value="${defaultSettings.steps}" />
<label for="sd_width">Width (<span id="sd_width_value"></span>)</label>
<input id="sd_width" type="range" max="${defaultSettings.dimension_max}" min="${defaultSettings.dimension_min}" step="${defaultSettings.dimension_step}" value="${defaultSettings.width}" />
<label for="sd_height">Height (<span id="sd_height_value"></span>)</label>
<input id="sd_height" type="range" max="${defaultSettings.dimension_max}" min="${defaultSettings.dimension_min}" step="${defaultSettings.dimension_step}" value="${defaultSettings.height}" />
<div><small>Only for Horde or remote Stable Diffusion Web UI:</small></div>
<div class="flex-container marginTop10 margin-bot-10px">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
Restore Faces
</label>
<label class="flex1 checkbox_label">
<input id="sd_enable_hr" type="checkbox" />
Hires. Fix
</label>
</div>
<label for="sd_model">Stable Diffusion model</label>
<select id="sd_model"></select>
<label for="sd_sampler">Sampling method</label>
<select id="sd_sampler"></select>
<div class="flex-container flexGap5 margin-bot-10px">
<label class="checkbox_label">
<input id="sd_horde_karras" type="checkbox" />
Karras (only for Horde, not all samplers supported)
</label>
</div>
<label for="sd_prompt_prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3"></textarea>
<div id="sd_character_prompt_block">
<label for="sd_character_prompt">Character-specific prompt prefix</label>
<small>Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
</div>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
</div>
</div>
<div class="inline-drawer-content">
<small><i>Use slash commands or the bottom Paintbrush button to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small>
<br>
<small><i>Hint: Save an API key in Horde KoboldAI API settings to use it here.</i></small>
<label for="sd_refine_mode" class="checkbox_label" title="Allow to edit prompts manually before sending them to generation API">
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
</label>
<div class="flex-container flexGap5 marginTop10 margin-bot-10px">
<label class="checkbox_label">
<input id="sd_horde" type="checkbox" />
Use Stable Horde
</label>
<label style="margin-left:1em;" class="checkbox_label">
<input id="sd_horde_nsfw" type="checkbox" />
Allow NSFW images from Horde
</label>
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>SD Prompt Templates</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="${defaultSettings.scale_min}" max="${defaultSettings.scale_max}" step="${defaultSettings.scale_step}" value="${defaultSettings.scale}" />
<label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="${defaultSettings.steps_min}" max="${defaultSettings.steps_max}" step="${defaultSettings.steps_step}" value="${defaultSettings.steps}" />
<label for="sd_width">Width (<span id="sd_width_value"></span>)</label>
<input id="sd_width" type="range" max="${defaultSettings.dimension_max}" min="${defaultSettings.dimension_min}" step="${defaultSettings.dimension_step}" value="${defaultSettings.width}" />
<label for="sd_height">Height (<span id="sd_height_value"></span>)</label>
<input id="sd_height" type="range" max="${defaultSettings.dimension_max}" min="${defaultSettings.dimension_min}" step="${defaultSettings.dimension_step}" value="${defaultSettings.height}" />
<div><small>Only for Horde or remote Stable Diffusion Web UI:</small></div>
<div class="flex-container marginTop10 margin-bot-10px">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
Restore Faces
</label>
<label class="flex1 checkbox_label">
<input id="sd_enable_hr" type="checkbox" />
Hires. Fix
</label>
<div id="sd_prompt_templates" class="inline-drawer-content">
</div>
<label for="sd_model">Stable Diffusion model</label>
<select id="sd_model"></select>
<label for="sd_sampler">Sampling method</label>
<select id="sd_sampler"></select>
<div class="flex-container flexGap5 margin-bot-10px">
<label class="checkbox_label">
<input id="sd_horde_karras" type="checkbox" />
Karras (only for Horde, not all samplers supported)
</label>
</div>
<label for="sd_prompt_prefix">Generated prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="2"></textarea>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="2"></textarea>
</div>
</div>`;
@ -835,16 +954,21 @@ jQuery(async () => {
$('#sd_restore_faces').on('input', onRestoreFacesInput);
$('#sd_enable_hr').on('input', onHighResFixInput);
$('#sd_refine_mode').on('input', onRefineModeInput);
$('#sd_character_prompt').on('input', onCharacterPromptInput);
$('#sd_character_prompt_block').hide();
$('.sd_settings .inline-drawer-toggle').on('click', function () {
initScrollHeight($("#sd_prompt_prefix"));
initScrollHeight($("#sd_negative_prompt"));
initScrollHeight($("#sd_character_prompt"));
})
eventSource.on(event_types.EXTRAS_CONNECTED, async () => {
await Promise.all([loadSamplers(), loadModels()]);
});
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
await loadSettings();
$('body').addClass('sd');
});

View File

@ -24,6 +24,6 @@
}
#sd_dropdown {
z-index: 3000;
z-index: 30000;
backdrop-filter: blur(--SmartThemeBlurStrength);
}
}

View File

@ -0,0 +1,403 @@
import { eventSource, event_types } from "../../../script.js"
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
export { CoquiTtsProvider }
function throwIfModuleMissing() {
if (!modules.includes('coqui-tts')) {
toastr.error(`Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.`)
throw new Error(`Coqui TTS module not loaded.`)
}
}
class CoquiTtsProvider {
//########//
// Config //
//########//
settings
voices = []
separator = ' .. '
defaultSettings = {
voiceMap: {}
}
get settingsHtml() {
let html = `
<div class="flex wide100p flexGap10 alignitemscenter">
<div style="flex: 80%;">
<label for="coqui_model">Model:</label>
<select id="coqui_model">
<option value="none">Select Model</option>
<!-- Add more model options here -->
</select>
</div>
<div class="flex justifyCenter" style="flex: 20%;">
<button id="coqui_preview" class="menu_button menu_button_icon wide100p" type="button">
</button>
</div>
</div>
<div class="flex wide100p flexGap10">
<div class="flex1">
<label for="coqui_speaker">Speaker:</label>
<select id="coqui_speaker">
<!-- Add more speaker options here -->
</select>
</div>
<div class="flex1">
<label for="coqui_language">Language:</label>
<select id="coqui_language">
<!-- Add more language options here -->
</select>
</div>
</div>
`
return html
}
onSettingsChange() {
}
loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
const modelSelect = document.getElementById('coqui_model');
const previewButton = document.getElementById('coqui_preview');
previewButton.addEventListener('click', () => {
const selectedModel = modelSelect.value;
this.sampleTtsVoice(selectedModel);
});//add event listener to button
previewButton.disabled = true;
previewButton.innerText = "Select Model";
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
}
const textexample = document.getElementById('tts_voice_map');
textexample.placeholder = 'Enter comma separated map of charName:ttsName[speakerID][langID]. Example: \nAqua:tts_models--en--ljspeech--glow-tts\model_file.pth,\nDarkness:tts_models--multilingual--multi-dataset--your_tts\model_file.pth[2][3]';
//Load models function
eventSource.on(event_types.EXTRAS_CONNECTED, () => {
this.getModels();
});
this.onttsCoquiHideButtons();
console.info("Settings loaded")
}
async onttsCoquiHideButtons() {
// Get references to the select element and the two input elements
const ttsProviderSelect = document.getElementById('tts_provider');
const ttsVoicesInput = document.getElementById('tts_voices');
const ttsPreviewInput = document.getElementById('tts_preview');
ttsProviderSelect.addEventListener('click', () => {
this.getModels();
});
// Add an event listener to the 'change' event of the tts_provider select element
ttsProviderSelect.addEventListener('change', () => {
// Check if the selected value is 'Coqui'
if (ttsProviderSelect.value === 'Coqui') {
ttsVoicesInput.style.display = 'none'; // Hide the tts_voices input
ttsPreviewInput.style.display = ''; // Show the tts_preview input
} else {
ttsVoicesInput.style.display = ''; // Show the tts_voices input
ttsPreviewInput.style.display = 'none'; // Hide the tts_preview input
}
});
}
async onApplyClick() {
return
}
async getLang() {
try {
const response = await doExtrasFetch(`${getApiUrl()}/api/coqui-tts/multlang`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const voiceData = await response.json();
const languageSelect = document.getElementById('coqui_language');
languageSelect.innerHTML = ''; // Clear existing options
if (Object.keys(voiceData).length === 0) {
const option = document.createElement('option');
option.value = 'none';
option.textContent = 'None';
languageSelect.appendChild(option);
} else {
for (const [key, value] of Object.entries(voiceData)) {
const option = document.createElement('option');
option.value = key;
option.textContent = key + ": " + value;
languageSelect.appendChild(option);
}
}
} catch (error) {
//console.error('Error fetching voice data:', error);
// Remove all options except "None"
const languageSelect = document.getElementById('coqui_language');
languageSelect.innerHTML = '';
const option = document.createElement('option');
option.value = 'none';
option.textContent = 'None';
languageSelect.appendChild(option);
}
}
async getSpeakers() {
try {
const response = await doExtrasFetch(`${getApiUrl()}/api/coqui-tts/multspeaker`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const voiceData = await response.json();
const speakerSelect = document.getElementById('coqui_speaker');
speakerSelect.innerHTML = ''; // Clear existing options
if (Object.keys(voiceData).length === 0) {
const option = document.createElement('option');
option.value = 'none';
option.textContent = 'None';
speakerSelect.appendChild(option);
} else {
for (const [index, name] of Object.entries(voiceData)) {
const option = document.createElement('option');
option.value = index;
option.textContent = index + ": " + name;
speakerSelect.appendChild(option);
}
}
} catch (error) {
//console.error('Error fetching voice data:', error);
// Remove all options except "None"
const speakerSelect = document.getElementById('coqui_speaker');
speakerSelect.innerHTML = '';
const option = document.createElement('option');
option.value = 'none';
option.textContent = 'None';
speakerSelect.appendChild(option);
}
}
async getModels() {
try {
throwIfModuleMissing();
const response = await doExtrasFetch(`${getApiUrl()}/api/coqui-tts/list`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const voiceIds = await response.json();
const modelSelect = document.getElementById('coqui_model');
if (voiceIds.length === 0) {
const option = document.createElement('option');
option.value = 'none';
option.textContent = 'Select Model';
modelSelect.appendChild(option);
} else {
voiceIds.forEach(voiceId => {
const option = document.createElement('option');
option.value = voiceId;
option.textContent = voiceId;
modelSelect.appendChild(option);
});
}
// Update provider endpoint on model selection change
modelSelect.addEventListener('change', () => {
const selectedModel = modelSelect.value;
this.LoadModel(selectedModel);
});
} catch (error) {
console.error('Error fetching voice IDs:', error);
// Add "None" option when the request fails or the response is empty
const modelSelect = document.getElementById('coqui_model');
const option = document.createElement('option');
option.value = 'none';
option.textContent = 'None';
modelSelect.appendChild(option);
}
}
async LoadModel(selectedModel) {
const previewButton = document.getElementById('coqui_preview');
previewButton.disabled = true;
previewButton.innerText = "Loading";
try {
throwIfModuleMissing();
const response = await doExtrasFetch(`${getApiUrl()}/api/coqui-tts/load?_model=${selectedModel}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.getSpeakers();
this.getLang();
const previewButton = document.getElementById('coqui_preview');
previewButton.disabled = false;
previewButton.innerText = "Play";
} catch (error) {
console.error('Error updating provider endpoint:', error);
}
}
async getVoice(voiceName) {
//tts_models--multilingual--multi-dataset--your_tts\model_file.pth[2][1]
//tts_models--en--ljspeech--glow-tts\model_file.pth
let _voiceNameOrg = voiceName; // Store the original voiceName in a variable _voiceNameOrg
voiceName = voiceName.replace(/(\[\d+\])+$/, ''); // For example, converts 'model[2][1]' to 'model'
this.voices = []; //reset for follow up runs
if (this.voices.length === 0) { this.voices = await this.fetchCheckMap(); }
// Search for a voice object in the 'this.voices' array where the 'name' property matches the provided 'voiceName'
//const match = this.voices.find((CoquiVoice) => CoquiVoice.name === voiceName);
const match = this.voices.find((CoquiVoice) => CoquiVoice.name === voiceName);
// If no match is found, throw an error indicating that the TTS Voice name was not found
if (!match) {
throw new Error(`TTS Voice name ${voiceName} not found`);
} else {
match.name = _voiceNameOrg;
match.voice_id = _voiceNameOrg;
}
// Return the matched voice object (with the 'name' property updated if a match was found)
return match;
}
async fetchCheckMap() {
const endpoint = `${getApiUrl()}/api/coqui-tts/checkmap`;
const response = await doExtrasFetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const voiceData = await response.json();
const voices = voiceData.map((voice) => ({
id: voice.name,
name: voice.id, // this is the issue!!!
voice_id: voice.id, // this is the issue!!!
//preview_url: false,
lang: voice.lang,
}));
return voices;
}
async fetchTtsVoiceIds() {
throwIfModuleMissing();
const endpoint = `${getApiUrl()}/api/coqui-tts/speaker_id`;
const response = await doExtrasFetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const voiceData = await response.json();
const voices = voiceData.map((voice) => ({
id: voice.name,
name: voice.id, //add filename here
voice_id: voice.id,
//preview_url: false,
//preview_url: `${getApiUrl()}/api/coqui-tts/download?model=${voice.id}`,
//http://localhost:5100/api/coqui-tts/speaker_id/tts_models/en/ljspeech/speedy-speech
lang: voice.lang,
}));
return voices;
}
sampleTtsVoice(voiceId) {
// Get the selected values of speaker and language
const speakerSelect = document.getElementById('coqui_speaker');
const languageSelect = document.getElementById('coqui_language');
const selectedSpeaker = speakerSelect.value;
const selectedLanguage = languageSelect.value;
// Construct the URL with the selected values
const url = `${getApiUrl()}/api/coqui-tts/tts?text=The%20Quick%20Brown%20Fox%20Jumps%20Over%20the%20Lazy%20Dog.&speaker_id=${voiceId}&style_wav=&language_id=${selectedLanguage}&mspker=${selectedSpeaker}`;
doExtrasFetch(url)
.then(response => response.blob())
.then(blob => {
const audioUrl = URL.createObjectURL(blob);
// Play the audio
const audio = new Audio(audioUrl);
audio.play();
})
.catch(error => {
console.error('Error performing TTS request:', error);
});
}
previewTtsVoice(voiceId) { //button on avail voices
throwIfModuleMissing();
const url = `${getApiUrl()}/api/coqui-tts/download?model=${voiceId}`;
doExtrasFetch(url)
.then(response => response.text()) // Expecting a text response
.then(responseText => {
const isResponseTrue = responseText.trim().toLowerCase() === 'true';
if (isResponseTrue) {
console.log("Downloading Model") //if true
} else {
console.error('Already Installed'); //if false
}
})
.catch(error => {
console.error('Error performing download:', error);
});
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId)
return response
}
async fetchTtsGeneration(inputText, voiceId) {
throwIfModuleMissing();
console.info(`Generating new TTS for voice_id ${voiceId}`);
const response = await doExtrasFetch(`${getApiUrl()}/api/coqui-tts/tts?text=${encodeURIComponent(inputText)}&speaker_id=${voiceId}`);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response
}
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -4,6 +4,7 @@ import { escapeRegex, getStringHash } from '../../utils.js'
import { EdgeTtsProvider } from './edge.js'
import { ElevenLabsTtsProvider } from './elevenlabs.js'
import { SileroTtsProvider } from './silerotts.js'
import { CoquiTtsProvider } from './coquitts.js'
import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js'
import { isMobile } from '../../RossAscends-mods.js'
@ -64,6 +65,7 @@ let ttsProviders = {
ElevenLabs: ElevenLabsTtsProvider,
Silero: SileroTtsProvider,
System: SystemTtsProvider,
Coqui: CoquiTtsProvider,
Edge: EdgeTtsProvider,
Novel: NovelTtsProvider,
}

View File

@ -11,13 +11,18 @@ export {
};
const nai_settings = {
temp_novel: 0.5,
rep_pen_novel: 1,
rep_pen_size_novel: 100,
rep_pen_slope_novel: 0,
rep_pen_freq_novel: 0,
rep_pen_presence_novel: 0,
tail_free_sampling_novel: 0.68,
temperature: 0.5,
repetition_penalty: 1,
repetition_penalty_range: 100,
repetition_penalty_slope: 0,
repetition_penalty_frequency: 0,
repetition_penalty_presence: 0,
tail_free_sampling: 0.68,
top_k: 0,
top_p: 1,
top_a: 1,
typical_p: 1,
min_length: 0,
model_novel: "euterpe-v2",
preset_settings_novel: "Classic-Euterpe",
streaming_novel: false,
@ -45,13 +50,20 @@ function loadNovelPreset(preset) {
$("#max_context_counter").text(`${preset.max_context}`);
$("#rep_pen_size_novel").attr('max', preset.max_context);
nai_settings.temp_novel = preset.temperature;
nai_settings.rep_pen_novel = preset.repetition_penalty;
nai_settings.rep_pen_size_novel = preset.repetition_penalty_range;
nai_settings.rep_pen_slope_novel = preset.repetition_penalty_slope;
nai_settings.rep_pen_freq_novel = preset.repetition_penalty_frequency;
nai_settings.rep_pen_presence_novel = preset.repetition_penalty_presence;
nai_settings.tail_free_sampling_novel = preset.tail_free_sampling;
nai_settings.temperature = preset.temperature;
nai_settings.repetition_penalty = preset.repetition_penalty;
nai_settings.repetition_penalty_range = preset.repetition_penalty_range;
nai_settings.repetition_penalty_slope = preset.repetition_penalty_slope;
nai_settings.repetition_penalty_frequency = preset.repetition_penalty_frequency;
nai_settings.repetition_penalty_presence = preset.repetition_penalty_presence;
nai_settings.tail_free_sampling = preset.tail_free_sampling;
nai_settings.top_k = preset.top_k;
nai_settings.top_p = preset.top_p;
nai_settings.top_a = preset.top_a;
nai_settings.typical_p = preset.typical_p;
nai_settings.min_length = preset.min_length;
nai_settings.cfg_scale = preset.cfg_scale;
nai_settings.phrase_rep_pen = preset.phrase_rep_pen;
loadNovelSettingsUi(nai_settings);
}
@ -59,33 +71,90 @@ function loadNovelSettings(settings) {
//load the rest of the Novel settings without any checks
nai_settings.model_novel = settings.model_novel;
$(`#model_novel_select option[value=${nai_settings.model_novel}]`).attr("selected", true);
$('#model_novel_select').val(nai_settings.model_novel);
nai_settings.temp_novel = settings.temp_novel;
nai_settings.rep_pen_novel = settings.rep_pen_novel;
nai_settings.rep_pen_size_novel = settings.rep_pen_size_novel;
nai_settings.rep_pen_slope_novel = settings.rep_pen_slope_novel;
nai_settings.rep_pen_freq_novel = settings.rep_pen_freq_novel;
nai_settings.rep_pen_presence_novel = settings.rep_pen_presence_novel;
nai_settings.tail_free_sampling_novel = settings.tail_free_sampling_novel;
nai_settings.preset_settings_novel = settings.preset_settings_novel;
nai_settings.temperature = settings.temperature;
nai_settings.repetition_penalty = settings.repetition_penalty;
nai_settings.repetition_penalty_range = settings.repetition_penalty_range;
nai_settings.repetition_penalty_slope = settings.repetition_penalty_slope;
nai_settings.repetition_penalty_frequency = settings.repetition_penalty_frequency;
nai_settings.repetition_penalty_presence = settings.repetition_penalty_presence;
nai_settings.tail_free_sampling = settings.tail_free_sampling;
nai_settings.top_k = settings.top_k;
nai_settings.top_p = settings.top_p;
nai_settings.top_a = settings.top_a;
nai_settings.typical_p = settings.typical_p;
nai_settings.min_length = settings.min_length;
nai_settings.phrase_rep_pen = settings.phrase_rep_pen;
nai_settings.cfg_scale = settings.cfg_scale;
nai_settings.streaming_novel = !!settings.streaming_novel;
loadNovelSettingsUi(nai_settings);
}
const phraseRepPenStrings = [
null,
"very_light",
"light",
"medium",
"aggressive",
"very_aggressive"
]
function getPhraseRepPenString(phraseRepPenCounter) {
if (phraseRepPenCounter < 1 || phraseRepPenCounter > F5) {
return null;
} else {
return phraseRepPenStrings[phraseRepPenCounter];
}
}
function getPhraseRepPenCounter(phraseRepPenString) {
if (phraseRepPenString === phraseRepPenStrings[1]) {
return 1;
} else if (phraseRepPenString === phraseRepPenStrings[2]) {
return 2;
} else if (phraseRepPenString === phraseRepPenStrings[3]) {
return 3;
} else if (phraseRepPenString === phraseRepPenStrings[4]) {
return 4;
} else if (phraseRepPenString === phraseRepPenStrings[5]) {
return 5;
} else {
return 0;
}
}
function loadNovelSettingsUi(ui_settings) {
$("#temp_novel").val(ui_settings.temp_novel);
$("#temp_counter_novel").text(Number(ui_settings.temp_novel).toFixed(2));
$("#rep_pen_novel").val(ui_settings.rep_pen_novel);
$("#rep_pen_counter_novel").text(Number(ui_settings.rep_pen_novel).toFixed(2));
$("#rep_pen_size_novel").val(ui_settings.rep_pen_size_novel);
$("#rep_pen_size_counter_novel").text(Number(ui_settings.rep_pen_size_novel).toFixed(0));
$("#rep_pen_slope_novel").val(ui_settings.rep_pen_slope_novel);
$("#rep_pen_slope_counter_novel").text(Number(`${ui_settings.rep_pen_slope_novel}`).toFixed(2));
$("#rep_pen_freq_novel").val(ui_settings.rep_pen_freq_novel);
$("#rep_pen_freq_counter_novel").text(Number(ui_settings.rep_pen_freq_novel).toFixed(5));
$("#rep_pen_presence_novel").val(ui_settings.rep_pen_presence_novel);
$("#rep_pen_presence_counter_novel").text(Number(ui_settings.rep_pen_presence_novel).toFixed(3));
$("#tail_free_sampling_novel").val(ui_settings.tail_free_sampling_novel);
$("#tail_free_sampling_counter_novel").text(Number(ui_settings.tail_free_sampling_novel).toFixed(3));
$("#temp_novel").val(ui_settings.temperature);
$("#temp_counter_novel").text(Number(ui_settings.temperature).toFixed(2));
$("#rep_pen_novel").val(ui_settings.repetition_penalty);
$("#rep_pen_counter_novel").text(Number(ui_settings.repetition_penalty).toFixed(2));
$("#rep_pen_size_novel").val(ui_settings.repetition_penalty_range);
$("#rep_pen_size_counter_novel").text(Number(ui_settings.repetition_penalty_range).toFixed(0));
$("#rep_pen_slope_novel").val(ui_settings.repetition_penalty_slope);
$("#rep_pen_slope_counter_novel").text(Number(`${ui_settings.repetition_penalty_slope}`).toFixed(2));
$("#rep_pen_freq_novel").val(ui_settings.repetition_penalty_frequency);
$("#rep_pen_freq_counter_novel").text(Number(ui_settings.repetition_penalty_frequency).toFixed(5));
$("#rep_pen_presence_novel").val(ui_settings.repetition_penalty_presence);
$("#rep_pen_presence_counter_novel").text(Number(ui_settings.repetition_penalty_presence).toFixed(3));
$("#tail_free_sampling_novel").val(ui_settings.tail_free_sampling);
$("#tail_free_sampling_counter_novel").text(Number(ui_settings.tail_free_sampling).toFixed(3));
$("#top_k_novel").val(ui_settings.top_k);
$("#top_k_counter_novel").text(Number(ui_settings.top_k).toFixed(0));
$("#top_p_novel").val(ui_settings.top_p);
$("#top_p_counter_novel").text(Number(ui_settings.top_p).toFixed(2));
$("#top_a_novel").val(ui_settings.top_a);
$("#top_a_counter_novel").text(Number(ui_settings.top_a).toFixed(2));
$("#typical_p_novel").val(ui_settings.typical_p);
$("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(2));
$("#cfg_scale_novel").val(ui_settings.cfg_scale);
$("#cfg_scale_counter_novel").text(Number(ui_settings.cfg_scale).toFixed(2));
$("#phrase_rep_pen_novel").val(getPhraseRepPenCounter(ui_settings.phrase_rep_pen));
$("#phrase_rep_pen_counter_novel").text(getPhraseRepPenCounter(ui_settings.phrase_rep_pen));
$("#min_length_novel").val(ui_settings.min_length);
$("#min_length_counter_novel").text(Number(ui_settings.min_length).toFixed(0));
$("#streaming_novel").prop('checked', ui_settings.streaming_novel);
}
@ -94,71 +163,117 @@ const sliders = [
sliderId: "#temp_novel",
counterId: "#temp_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.temp_novel = Number(val).toFixed(2); },
setValue: (val) => { nai_settings.temperature = Number(val).toFixed(2); },
},
{
sliderId: "#rep_pen_novel",
counterId: "#rep_pen_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.rep_pen_novel = Number(val).toFixed(2); },
setValue: (val) => { nai_settings.repetition_penalty = Number(val).toFixed(2); },
},
{
sliderId: "#rep_pen_size_novel",
counterId: "#rep_pen_size_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.rep_pen_size_novel = Number(val).toFixed(0); },
setValue: (val) => { nai_settings.repetition_penalty_range = Number(val).toFixed(0); },
},
{
sliderId: "#rep_pen_slope_novel",
counterId: "#rep_pen_slope_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.rep_pen_slope_novel = Number(val).toFixed(2); },
setValue: (val) => { nai_settings.repetition_penalty_slope = Number(val).toFixed(2); },
},
{
sliderId: "#rep_pen_freq_novel",
counterId: "#rep_pen_freq_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.rep_pen_freq_novel = Number(val).toFixed(5); },
setValue: (val) => { nai_settings.repetition_penalty_frequency = Number(val).toFixed(5); },
},
{
sliderId: "#rep_pen_presence_novel",
counterId: "#rep_pen_presence_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.rep_pen_presence_novel = Number(val).toFixed(3); },
setValue: (val) => { nai_settings.repetition_penalty_presence = Number(val).toFixed(3); },
},
{
sliderId: "#tail_free_sampling_novel",
counterId: "#tail_free_sampling_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.tail_free_sampling_novel = Number(val).toFixed(3); },
setValue: (val) => { nai_settings.tail_free_sampling = Number(val).toFixed(3); },
},
{
sliderId: "#top_k_novel",
counterId: "#top_k_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.top_k = Number(val).toFixed(0); },
},
{
sliderId: "#top_p_novel",
counterId: "#top_p_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.top_p = Number(val).toFixed(2); },
},
{
sliderId: "#top_a_novel",
counterId: "#top_a_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.top_a = Number(val).toFixed(2); },
},
{
sliderId: "#typical_p_novel",
counterId: "#typical_p_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(2); },
},
{
sliderId: "#cfg_scale_novel",
counterId: "#cfg_scale_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.cfg_scale = Number(val).toFixed(2); },
},
{
sliderId: "#phrase_rep_pen_novel",
counterId: "#phrase_rep_pen_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.phrase_rep_pen = getPhraseRepPenString(Number(val).toFixed(0)); },
},
{
sliderId: "#min_length_novel",
counterId: "#min_length_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.min_length = Number(val).toFixed(0); },
},
];
export function getNovelGenerationData(finalPromt, this_settings, this_amount_gen) {
const isNewModel = (nai_settings.model_novel.includes('clio') || nai_settings.model_novel.includes('kayra'));
return {
"input": finalPromt,
"model": nai_settings.model_novel,
"use_string": true,
"temperature": parseFloat(nai_settings.temp_novel),
"temperature": parseFloat(nai_settings.temperature),
"max_length": this_amount_gen, // this_settings.max_length, // <= why?
"min_length": this_settings.min_length,
"tail_free_sampling": parseFloat(nai_settings.tail_free_sampling_novel),
"repetition_penalty": parseFloat(nai_settings.rep_pen_novel),
"repetition_penalty_range": parseInt(nai_settings.rep_pen_size_novel),
"repetition_penalty_slope": parseFloat(nai_settings.rep_pen_slope_novel),
"repetition_penalty_frequency": parseFloat(nai_settings.rep_pen_freq_novel),
"repetition_penalty_presence": parseFloat(nai_settings.rep_pen_presence_novel),
"top_a": this_settings.top_a,
"top_p": this_settings.top_p,
"top_k": this_settings.top_k,
"typical_p": this_settings.typical_p,
"min_length": parseInt(nai_settings.min_length),
"tail_free_sampling": parseFloat(nai_settings.tail_free_sampling),
"repetition_penalty": parseFloat(nai_settings.repetition_penalty),
"repetition_penalty_range": parseInt(nai_settings.repetition_penalty_range),
"repetition_penalty_slope": parseFloat(nai_settings.repetition_penalty_slope),
"repetition_penalty_frequency": parseFloat(nai_settings.repetition_penalty_frequency),
"repetition_penalty_presence": parseFloat(nai_settings.repetition_penalty_presence),
"top_a": parseFloat(nai_settings.top_a),
"top_p": parseFloat(nai_settings.top_p),
"top_k": parseInt(nai_settings.top_k),
"typical_p": parseFloat(nai_settings.typical_p),
"cfg_scale": parseFloat(nai_settings.cfg_scale),
"cfg_uc": "",
"phrase_rep_pen": nai_settings.phrase_rep_pen,
//"stop_sequences": {{187}},
//bad_words_ids = {{50256}, {0}, {1}};
"generate_until_sentence": true,
"use_cache": false,
"use_string": true,
"return_full_text": false,
"prefix": "vanilla",
"prefix": isNewModel ? "special_instruct" : "vanilla",
"order": this_settings.order,
"streaming": nai_settings.streaming_novel,
};
@ -212,7 +327,7 @@ $(document).ready(function () {
const value = $(this).val();
const formattedValue = slider.format(value);
slider.setValue(value);
$(slider.counterId).html(formattedValue);
$(slider.counterId).text(formattedValue);
console.log('saving');
saveSettingsDebounced();
});

View File

@ -142,6 +142,7 @@ const default_settings = {
max_context_unlocked: false,
api_url_scale: '',
show_external_models: false,
proxy_password: '',
};
const oai_settings = {
@ -178,6 +179,7 @@ const oai_settings = {
max_context_unlocked: false,
api_url_scale: '',
show_external_models: false,
proxy_password: '',
};
let openai_setting_names;
@ -400,7 +402,8 @@ async function prepareOpenAIMessages({ systemPrompt, name2, storyString, worldIn
const jailbreak = power_user.prefer_character_jailbreak && jailbreakPrompt ? jailbreakPrompt : oai_settings.jailbreak_prompt;
if (oai_settings.jailbreak_system && jailbreak) {
const jailbreakMessage = { "role": "system", "content": substituteParams(jailbreak, name1, name2, oai_settings.jailbreak_prompt) };
const jbContent = substituteParams(jailbreak, name1, name2, oai_settings.jailbreak_prompt).replace(/\r/gm, '').trim();
const jailbreakMessage = { "role": "system", "content": jbContent };
openai_msgs.push(jailbreakMessage);
total_count += handler_instance.count([jailbreakMessage], true, 'jailbreak');
@ -766,6 +769,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) {
validateReverseProxy();
generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
generate_data['proxy_password'] = oai_settings.proxy_password;
}
if (isClaude) {
@ -1104,6 +1108,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;
oai_settings.api_url_scale = settings.api_url_scale ?? default_settings.api_url_scale;
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
if (settings.nsfw_toggle !== undefined) oai_settings.nsfw_toggle = !!settings.nsfw_toggle;
if (settings.keep_example_dialogue !== undefined) oai_settings.keep_example_dialogue = !!settings.keep_example_dialogue;
@ -1115,6 +1120,7 @@ function loadOpenAISettings(data, settings) {
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password);
$('#model_openai_select').val(oai_settings.openai_model);
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
@ -1168,9 +1174,7 @@ function loadOpenAISettings(data, settings) {
if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
$('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
if (oai_settings.reverse_proxy !== '') {
$("#ReverseProxyWarningMessage").css('display', 'block');
}
$(".reverse_proxy_warning").toggle(oai_settings.reverse_proxy !== '');
$('#openai_logit_bias_preset').empty();
for (const preset of Object.keys(oai_settings.bias_presets)) {
@ -1211,6 +1215,7 @@ async function getStatusOpen() {
let data = {
reverse_proxy: oai_settings.reverse_proxy,
proxy_password: oai_settings.proxy_password,
use_openrouter: oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER,
};
@ -1310,6 +1315,7 @@ async function saveOpenAIPreset(name, settings) {
impersonation_prompt: settings.impersonation_prompt,
bias_preset_selected: settings.bias_preset_selected,
reverse_proxy: settings.reverse_proxy,
proxy_password: settings.proxy_password,
legacy_streaming: settings.legacy_streaming,
max_context_unlocked: settings.max_context_unlocked,
nsfw_avoidance_prompt: settings.nsfw_avoidance_prompt,
@ -1649,6 +1655,7 @@ function onSettingsPresetChange() {
stream_openai: ['#stream_toggle', 'stream_openai', true],
api_url_scale: ['#api_url_scale', 'api_url_scale', false],
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
};
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
@ -1857,9 +1864,7 @@ async function onNewPresetClick() {
function onReverseProxyInput() {
oai_settings.reverse_proxy = $(this).val();
if (oai_settings.reverse_proxy == '') {
$("#ReverseProxyWarningMessage").css('display', 'none');
} else { $("#ReverseProxyWarningMessage").css('display', 'block'); }
$(".reverse_proxy_warning").toggle(oai_settings.reverse_proxy != '');
saveSettingsDebounced();
}
@ -1911,7 +1916,7 @@ async function onConnectButtonClick(e) {
await writeSecret(SECRET_KEYS.CLAUDE, api_key_claude);
}
if (!secret_state[SECRET_KEYS.CLAUDE]) {
if (!secret_state[SECRET_KEYS.CLAUDE] && !oai_settings.reverse_proxy) {
console.log('No secret key saved for Claude');
return;
}
@ -1924,7 +1929,7 @@ async function onConnectButtonClick(e) {
await writeSecret(SECRET_KEYS.OPENAI, api_key_openai);
}
if (!secret_state[SECRET_KEYS.OPENAI]) {
if (!secret_state[SECRET_KEYS.OPENAI] && !oai_settings.reverse_proxy) {
console.log('No secret key saved for OpenAI');
return;
}
@ -2196,6 +2201,11 @@ $(document).ready(function () {
saveSettingsDebounced();
});
$('#openai_proxy_password').on('input', function () {
oai_settings.proxy_password = $(this).val();
saveSettingsDebounced();
});
$("#api_button_openai").on("click", onConnectButtonClick);
$("#openai_reverse_proxy").on("input", onReverseProxyInput);
$("#model_openai_select").on("change", onModelChange);

View File

@ -43,7 +43,7 @@ export {
send_on_enter_options,
};
const MAX_CONTEXT_DEFAULT = 4096;
export const MAX_CONTEXT_DEFAULT = 4096;
const MAX_CONTEXT_UNLOCKED = 65536;
const avatar_styles = {
@ -186,6 +186,7 @@ let power_user = {
persona_description_position: persona_description_positions.BEFORE_CHAR,
custom_stopping_strings: '',
fuzzy_search: false,
};
let themes = [];
@ -255,7 +256,7 @@ function fixMarkdown(text) {
// i.e. "^example * text* * harder problem *\n" -> "^example *text* *harder problem*\n"
// Find pairs of formatting characters and capture the text in between them
const format = /(\*|_|~){1,2}([\s\S]*?)\1{1,2}/gm;
const format = /([\*_]{1,2})([\s\S]*?)\1/gm;
let matches = [];
let match;
while ((match = format.exec(text)) !== null) {
@ -266,7 +267,7 @@ function fixMarkdown(text) {
let newText = text;
for (let i = matches.length - 1; i >= 0; i--) {
let matchText = matches[i][0];
let replacementText = matchText.replace(/(\*|_|~)(\s+)|(\s+)(\*|_|~)/g, '$1$4');
let replacementText = matchText.replace(/(\*|_)([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)|([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)(\*|_)/g, '$1$4');
newText = newText.slice(0, matches[i].index) + replacementText + newText.slice(matches[i].index + matchText.length);
}
@ -534,6 +535,11 @@ async function applyTheme(name) {
{
key: 'chat_width',
action: async () => {
// If chat width is not set, set it to 50
if (!power_user.chat_width) {
power_user.chat_width = 50;
}
localStorage.setItem(storage_keys.chat_width, power_user.chat_width);
applyChatWidth();
}
@ -660,6 +666,10 @@ function loadPowerUserSettings(settings, data) {
power_user.waifuMode = false;
}
if (power_user.chat_width === '') {
power_user.chat_width = 50;
}
$('#trim_spaces').prop("checked", power_user.trim_spaces);
$('#continue_on_send').prop("checked", power_user.continue_on_send);
$('#auto_swipe').prop("checked", power_user.auto_swipe);
@ -667,6 +677,7 @@ function loadPowerUserSettings(settings, data) {
$('#auto_swipe_blacklist').val(power_user.auto_swipe_blacklist.join(", "));
$('#auto_swipe_blacklist_threshold').val(power_user.auto_swipe_blacklist_threshold);
$('#custom_stopping_strings').val(power_user.custom_stopping_strings);
$('#fuzzy_search_checkbox').prop("checked", power_user.fuzzy_search);
$("#console_log_prompts").prop("checked", power_user.console_log_prompts);
$('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown);
@ -887,6 +898,31 @@ function loadInstructMode() {
});
}
export function fuzzySearchCharacters(searchValue) {
const fuse = new Fuse(characters, {
keys: [
{ name: 'data.name', weight: 5 },
{ name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 },
{ name: 'data.personality', weight: 2 },
{ name: 'data.first_mes', weight: 2 },
{ name: 'data.creator_notes', weight: 2 },
{ name: 'data.creator', weight: 1 },
{ name: 'data.tags', weight: 1 },
{ name: 'data.alternate_greetings', weight: 1 }
],
includeScore: true,
ignoreLocation: true,
threshold: 0.2,
});
const results = fuse.search(searchValue);
console.debug('Fuzzy search results for ' + searchValue, results)
const indices = results.map(x => x.refIndex);
return indices;
}
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2) {
const includeNames = isNarrator ? false : (power_user.instruct.names || !!selected_group || !!forceAvatar);
let sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
@ -1151,10 +1187,10 @@ async function resetMovablePanels(type) {
//if happening as part of preset application, do it quietly.
if (type === 'quiet') {
return
//if happening due to resize, tell user.
//if happening due to resize, tell user.
} else if (type === 'resize') {
toastr.warning('Panel positions reset due to zoom/resize');
//if happening due to manual button press
//if happening due to manual button press
} else {
toastr.success('Panel positions reset');
}
@ -1173,7 +1209,7 @@ function doNewChat() {
function doRandomChat() {
resetSelectedGroup();
setCharacterId(Math.floor(Math.random() * characters.length));
setCharacterId(Math.floor(Math.random() * characters.length).toString());
setTimeout(() => {
reloadCurrentChat();
}, 1);
@ -1963,6 +1999,11 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#fuzzy_search_checkbox').on('input', function () {
power_user.fuzzy_search = !!$(this).prop('checked');
saveSettingsDebounced();
});
$(window).on('focus', function () {
browser_has_focus = true;
});

View File

@ -0,0 +1,351 @@
import {
amount_gen,
callPopup,
characters,
eventSource,
event_types,
getRequestHeaders,
koboldai_setting_names,
koboldai_settings,
main_api,
max_context,
nai_settings,
novelai_setting_names,
novelai_settings,
saveSettingsDebounced,
this_chid,
} from "../script.js";
import { groups, selected_group } from "./group-chats.js";
import { kai_settings } from "./kai-settings.js";
import {
textgenerationwebui_preset_names,
textgenerationwebui_presets,
textgenerationwebui_settings,
} from "./textgen-settings.js";
import { download, parseJsonFile, waitUntilCondition } from "./utils.js";
const presetManagers = {};
function autoSelectPreset() {
const presetManager = getPresetManager();
if (!presetManager) {
console.debug(`Preset Manager not found for API: ${main_api}`);
return;
}
const name = selected_group ? groups.find(x => x.id == selected_group)?.name : characters[this_chid]?.name;
if (!name) {
console.debug(`Preset candidate not found for API: ${main_api}`);
return;
}
const preset = presetManager.findPreset(name);
const selectedPreset = presetManager.getSelectedPreset();
if (preset === selectedPreset) {
console.debug(`Preset already selected for API: ${main_api}, name: ${name}`);
return;
}
if (preset !== undefined && preset !== null) {
console.log(`Preset found for API: ${main_api}, name: ${name}`);
presetManager.selectPreset(preset);
}
}
function getPresetManager() {
const apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
if (!Object.keys(presetManagers).includes(apiId)) {
return null;
}
return presetManagers[apiId];
}
function registerPresetManagers() {
$('select[data-preset-manager-for]').each((_, e) => {
const forData = $(e).data("preset-manager-for");
for (const apiId of forData.split(",")) {
console.debug(`Registering preset manager for API: ${apiId}`);
presetManagers[apiId] = new PresetManager($(e), apiId);
}
});
}
class PresetManager {
constructor(select, apiId) {
this.select = select;
this.apiId = apiId;
}
findPreset(name) {
return $(this.select).find(`option:contains(${name})`).val();
}
getSelectedPreset() {
return $(this.select).find("option:selected").val();
}
getSelectedPresetName() {
return $(this.select).find("option:selected").text();
}
selectPreset(preset) {
$(this.select).find(`option[value=${preset}]`).prop('selected', true);
$(this.select).val(preset).trigger("change");
}
async updatePreset() {
const selected = $(this.select).find("option:selected");
if (selected.val() == 'gui') {
toastr.info('Cannot update GUI preset');
return;
}
const name = selected.text();
await this.savePreset(name);
toastr.success('Preset updated');
}
async savePresetAs() {
const popupText = `
<h3>Preset name:</h3>
<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>`;
const name = await callPopup(popupText, "input");
await this.savePreset(name);
toastr.success('Preset saved');
}
async savePreset(name, settings) {
const preset = settings ?? this.getPresetSettings();
const res = await fetch(`/save_preset`, {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({ preset, name, apiId: this.apiId })
});
if (!res.ok) {
toastr.error('Failed to save preset');
}
const data = await res.json();
name = data.name;
this.updateList(name, preset);
}
getPresetList() {
let presets = [];
let preset_names = {};
switch (this.apiId) {
case "koboldhorde":
case "kobold":
presets = koboldai_settings;
preset_names = koboldai_setting_names;
break;
case "novel":
presets = novelai_settings;
preset_names = novelai_setting_names;
break;
case "textgenerationwebui":
presets = textgenerationwebui_presets;
preset_names = textgenerationwebui_preset_names;
break;
default:
console.warn(`Unknown API ID ${this.apiId}`);
}
return { presets, preset_names };
}
updateList(name, preset) {
const { presets, preset_names } = this.getPresetList();
const presetExists = this.apiId == "textgenerationwebui" ? preset_names.includes(name) : Object.keys(preset_names).includes(name);
if (presetExists) {
if (this.apiId == "textgenerationwebui") {
presets[preset_names.indexOf(name)] = preset;
$(this.select).find(`option[value="${name}"]`).prop('selected', true);
$(this.select).val(name).trigger("change");
}
else {
const value = preset_names[name];
presets[value] = preset;
$(this.select).find(`option[value="${value}"]`).prop('selected', true);
$(this.select).val(value).trigger("change");
}
}
else {
presets.push(preset);
const value = presets.length - 1;
// ooba is reversed
if (this.apiId == "textgenerationwebui") {
preset_names[value] = name;
const option = $('<option></option>', { value: name, text: name, selected: true });
$(this.select).append(option);
$(this.select).val(name).trigger("change");
} else {
preset_names[name] = value;
const option = $('<option></option>', { value: value, text: name, selected: true });
$(this.select).append(option);
$(this.select).val(value).trigger("change");
}
}
}
getPresetSettings() {
function getSettingsByApiId(apiId) {
switch (apiId) {
case "koboldhorde":
case "kobold":
return kai_settings;
case "novel":
return nai_settings;
case "textgenerationwebui":
return textgenerationwebui_settings;
default:
console.warn(`Unknown API ID ${apiId}`);
return {};
}
}
const filteredKeys = ['preset', 'streaming_url', 'stopping_strings', 'use_stop_sequence'];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
for (const key of filteredKeys) {
if (settings.hasOwnProperty(key)) {
delete settings[key];
}
}
settings['genamt'] = amount_gen;
settings['max_length'] = max_context;
return settings;
}
async deleteCurrentPreset() {
const { presets, preset_names } = this.getPresetList();
const value = this.getSelectedPreset();
const nameToDelete = this.getSelectedPresetName();
if (value == 'gui') {
toastr.info('Cannot delete GUI preset');
return;
}
$(this.select).find(`option[value="${value}"]`).remove();
if (this.apiId == "textgenerationwebui") {
preset_names.splice(preset_names.indexOf(value), 1);
} else {
delete preset_names[nameToDelete];
}
if (Object.keys(preset_names).length) {
const nextPresetName = Object.keys(preset_names)[0];
const newValue = preset_names[nextPresetName];
$(this.select).find(`option[value="${newValue}"]`).attr('selected', true);
$(this.select).trigger('change');
}
const response = await fetch('/delete_preset', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ name: nameToDelete, apiId: this.apiId }),
});
if (!response.ok) {
toastr.warning('Preset was not deleted from server');
} else {
toastr.success('Preset deleted');
}
}
}
jQuery(async () => {
await waitUntilCondition(() => eventSource !== undefined);
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
registerPresetManagers();
$(document).on("click", "[data-preset-manager-update]", async function () {
const presetManager = getPresetManager();
if (!presetManager) {
return;
}
await presetManager.updatePreset();
});
$(document).on("click", "[data-preset-manager-new]", async function () {
const presetManager = getPresetManager();
if (!presetManager) {
return;
}
await presetManager.savePresetAs();
});
$(document).on("click", "[data-preset-manager-export]", async function () {
const presetManager = getPresetManager();
if (!presetManager) {
return;
}
const selected = $(presetManager.select).find("option:selected");
const name = selected.text();
const preset = presetManager.getPresetSettings();
const data = JSON.stringify(preset, null, 4);
download(data, `${name}.json`, "application/json");
});
$(document).on("click", "[data-preset-manager-import]", async function () {
$('[data-preset-manager-file]').trigger('click');
});
$(document).on("change", "[data-preset-manager-file]", async function (e) {
const presetManager = getPresetManager();
if (!presetManager) {
return;
}
const file = e.target.files[0];
if (!file) {
return;
}
const name = file.name.replace('.json', '').replace('.settings', '');
const data = await parseJsonFile(file);
await presetManager.savePreset(name, data);
toastr.success('Preset imported');
e.target.value = null;
});
$(document).on("click", "[data-preset-manager-delete]", async function () {
const presetManager = getPresetManager();
if (!presetManager) {
return;
}
const confirm = await callPopup('Delete the preset? This action is irreversible and your current settings will be overwritten.', 'confirm');
if (!confirm) {
return;
}
await presetManager.deleteCurrentPreset();
saveSettingsDebounced();
});
})

View File

@ -20,6 +20,7 @@ import {
setCharacterId,
generateQuietPrompt,
reloadCurrentChat,
sendMessageAsUser,
} from "../script.js";
import { humanizedDateTime } from "./RossAscends-mods.js";
import { resetSelectedGroup } from "./group-chats.js";
@ -126,11 +127,23 @@ parser.addCommand('continue', continueChatCallback, ['cont'], ' continues th
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> opens up a chat with the character by its name', true, true);
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> generates a system message using a specified prompt', true, true);
parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> deletes all messages attributed to a specified name', true, true);
parser.addCommand('send', sendUserMessageCallback, ['add'], '<span class="monospace">(text)</span> adds a user message to the chat log without triggering a generation', true, true);
const NARRATOR_NAME_KEY = 'narrator_name';
const NARRATOR_NAME_DEFAULT = 'System';
const COMMENT_NAME_DEFAULT = 'Note';
async function sendUserMessageCallback(_, text) {
if (!text) {
console.warn('WARN: No text provided for /send command');
return;
}
text = text.trim();
const bias = extractMessageBias(text);
sendMessageAsUser(text, bias);
}
async function deleteMessagesByNameCallback(_, name) {
if (!name) {
console.warn('WARN: No name provided for /delname command');
@ -192,7 +205,7 @@ function goToCharacterCallback(_, name) {
const characterIndex = findCharacterIndex(name);
if (characterIndex !== -1) {
openChat(characterIndex);
openChat(new String(characterIndex));
} else {
console.warn(`No matches found for name "${name}"`);
}

View File

@ -3,6 +3,7 @@ import {
getStoppingStrings,
max_context,
saveSettingsDebounced,
setGenerationParamsFromPreset,
} from "../script.js";
export {
@ -11,7 +12,7 @@ export {
generateTextGenWithStreaming,
}
let textgenerationwebui_settings = {
const textgenerationwebui_settings = {
temp: 0.7,
top_p: 0.5,
top_k: 40,
@ -44,8 +45,8 @@ let textgenerationwebui_settings = {
mirostat_eta: 0.1,
};
let textgenerationwebui_presets = [];
let textgenerationwebui_preset_names = [];
export let textgenerationwebui_presets = [];
export let textgenerationwebui_preset_names = [];
const setting_names = [
"temp",
@ -89,6 +90,7 @@ function selectPreset(name) {
const value = preset[name];
setSettingByName(name, value, true);
}
setGenerationParamsFromPreset(preset);
saveSettingsDebounced();
}

View File

@ -970,7 +970,7 @@ async function checkWorldInfo(chat, maxContext) {
if (selectiveLogic === 0) {
console.debug('saw AND logic, checking..')
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
console.log(`activating entry ${entry.uid} with AND found`)
console.debug(`activating entry ${entry.uid} with AND found`)
activatedNow.add(entry);
break secondary;
}
@ -1250,7 +1250,7 @@ export function checkEmbeddedWorld(chid) {
const worldName = characters[chid]?.data?.extensions?.world;
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
toastr.info(
'To import and use it, select "Import Embedded World Info" in the Options dropdown menu on the character panel.',
'To import and use it, select "Import Card Lore" in the "More..." dropdown menu on the character panel.',
`${characters[chid].name} has an embedded World/Lorebook`,
{ timeOut: 10000, extendedTimeOut: 20000, positionClass: 'toast-top-center' },
);

View File

@ -1512,8 +1512,20 @@ input[type=search]:focus::-webkit-search-cancel-button {
font-weight: bolder;
}
.character_name_block {
display: flex;
align-items: baseline;
flex-direction: row;
gap: 5px;
}
.ch_avatar_url {
float: right;
font-style: italic;
}
body.charListGrid #rm_print_characters_block .character_version,
body.charListGrid #rm_print_characters_block .ch_avatar_url {
display: none;
}
.character_select .avatar {
@ -1547,7 +1559,7 @@ body.big-avatars .ch_description {
/*applies to both groups and solos chars in the char list*/
#rm_print_characters_block .ch_name {
width: 100%;
flex: 1;
white-space: nowrap;
/* max-width: calc(100% - 29px); */
overflow: hidden;
@ -4243,6 +4255,10 @@ toolcool-color-picker {
padding: 5px;
}
.flex {
display: flex;
}
.flex-container {
display: flex;
gap: 5px;
@ -4500,14 +4516,16 @@ toolcool-color-picker {
flex-direction: column;
}
.openai_preset_buttons {
.openai_preset_buttons,
.preset_buttons {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 5px;
}
.openai_preset_buttons select {
.openai_preset_buttons select,
.preset_buttons select {
flex-grow: 1;
}
@ -4849,6 +4867,10 @@ body.waifuMode .zoomed_avatar {
gap: 5px;
}
.flexGap10 {
gap: 10px;
}
.timestamp {
font-size: calc(var(--mainFontSize) * 0.7);
font-weight: 400;
@ -5422,4 +5444,4 @@ body.waifuMode .zoomed_avatar {
background-color: var(--SmartThemeBlurTintColor);
text-align: center;
line-height: 14px;
}
}

135
server.js
View File

@ -1108,7 +1108,7 @@ app.post("/editcharacterattribute", jsonParser, async function (request, respons
}
});
app.post("/deletecharacter", urlencodedParser, async function (request, response) {
app.post("/deletecharacter", jsonParser, async function (request, response) {
if (!request.body || !request.body.avatar_url) {
return response.sendStatus(400);
}
@ -1224,6 +1224,11 @@ const calculateChatSize = (charDir) => {
return { chatSize, dateLastChat };
}
// Calculate the total string length of the data object
const calculateDataSize = (data) => {
return typeof data === 'object' ? Object.values(data).reduce((acc, val) => acc + new String(val).length, 0) : 0;
}
/**
* processCharacter - Process a given character, read its data and calculate its statistics.
*
@ -1245,6 +1250,7 @@ const processCharacter = async (item, i) => {
const { chatSize, dateLastChat } = calculateChatSize(char_dir);
characters[i]['chat_size'] = chatSize;
characters[i]['date_last_chat'] = dateLastChat;
characters[i]['data_size'] = calculateDataSize(jsonObject?.data);
}
catch (err) {
characters[i] = {
@ -1780,7 +1786,7 @@ app.post("/generate_novelai", jsonParser, async function (request, response_gene
console.log(request.body);
const bw = require('./src/bad-words');
const bad_words_ids = request.body.model.includes('clio') ? bw.clioBadWordsId : bw.badWordIds;
const isNewModel = (request.body.model.includes('clio') || request.body.model.includes('kayra'));
const data = {
"input": request.body.input,
"model": request.body.model,
@ -1799,13 +1805,16 @@ app.post("/generate_novelai", jsonParser, async function (request, response_gene
"top_p": request.body.top_p,
"top_k": request.body.top_k,
"typical_p": request.body.typical_p,
"cfg_scale": request.body.cfg_scale,
"cfg_uc": request.body.cfg_uc,
"phrase_rep_pen": request.body.phrase_rep_pen,
//"stop_sequences": {{187}},
"bad_words_ids": bad_words_ids,
"bad_words_ids": isNewModel ? bw.clioBadWordsId : bw.badWordIds,
//generate_until_sentence = true;
"use_cache": request.body.use_cache,
"use_string": true,
"return_full_text": request.body.return_full_text,
"prefix": request.body.prefix,
"prefix": isNewModel ? "special_instruct" : request.body.prefix,
"order": request.body.order
}
};
@ -2849,7 +2858,7 @@ app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_
if (request.body.use_openrouter == false) {
api_url = new URL(request.body.reverse_proxy || api_openai).toString();
api_key_openai = readSecret(SECRET_KEYS.OPENAI);
api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.OPENAI);
headers = {};
} else {
api_url = 'https://openrouter.ai/api/v1';
@ -2858,7 +2867,7 @@ app.post("/getstatus_openai", jsonParser, function (request, response_getstatus_
headers = { 'HTTP-Referer': request.headers.referer };
}
if (!api_key_openai) {
if (!api_key_openai && !request.body.reverse_proxy) {
return response_getstatus_openai.status(401).send({ error: true });
}
@ -2927,44 +2936,6 @@ app.post("/openai_bias", jsonParser, async function (request, response) {
return response.send(result);
});
// TODO: Dead code, consider deleting. Users will get redirected to OpenAI site instead.
// 'Your request to GET /v1/dashboard/billing/usage must be made with a session key (that is, it can only be made from the browser). You made it with the following key type: secret.'
app.post("/openai_usage", jsonParser, async function (request, response) {
if (!request.body) return response.sendStatus(400);
const key = readSecret(SECRET_KEYS.OPENAI);
if (!key) {
console.warn('Get key usage failed: Missing OpenAI API key.');
return response.sendStatus(401);
}
const api_url = new URL(request.body.reverse_proxy || api_openai).toString();
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${key}`,
};
const date = new Date();
date.setDate(1);
const start_date = date.toISOString().slice(0, 10);
date.setMonth(date.getMonth() + 1);
const end_date = date.toISOString().slice(0, 10);
try {
const res = await getAsync(
`${api_url}/dashboard/billing/usage?start_date=${start_date}&end_date=${end_date}`,
{ headers },
);
return response.send(res);
}
catch (error) {
console.log(error);
return response.sendStatus(400);
}
});
app.post("/deletepreset_openai", jsonParser, function (request, response) {
if (!request.body || !request.body.name) {
return response.sendStatus(400);
@ -3093,7 +3064,7 @@ async function sendClaudeRequest(request, response) {
const fetch = require('node-fetch').default;
const api_url = new URL(request.body.reverse_proxy || api_claude).toString();
const api_key_claude = readSecret(SECRET_KEYS.CLAUDE);
const api_key_claude = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.CLAUDE);
if (!api_key_claude) {
return response.status(401).send({ error: true });
@ -3182,7 +3153,7 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
if (!request.body.use_openrouter) {
api_url = new URL(request.body.reverse_proxy || api_openai).toString();
api_key_openai = readSecret(SECRET_KEYS.OPENAI);
api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.OPENAI);
headers = {};
} else {
api_url = 'https://openrouter.ai/api/v1';
@ -3191,7 +3162,7 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
headers = { 'HTTP-Referer': request.headers.referer };
}
if (!api_key_openai) {
if (!api_key_openai && !request.body.reverse_proxy) {
return response_generate_openai.status(401).send({ error: true });
}
@ -3289,6 +3260,10 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
message = 'API key disabled or exhausted';
console.log(message);
break;
case 451:
message = error?.response?.data?.error?.message || 'Unavailable for legal reasons';
console.log(message);
break;
}
const quota_error = error?.response?.status === 429 && error?.response?.data?.error?.type === 'insufficient_quota';
@ -3341,6 +3316,47 @@ app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_op
response_tokenize_openai.send({ "token_count": num_tokens });
});
app.post("/save_preset", jsonParser, function (request, response) {
const name = sanitize(request.body.name);
if (!request.body.preset || !name) {
return response.sendStatus(400);
}
const filename = `${name}.settings`;
const directory = getPresetFolderByApiId(request.body.apiId);
if (!directory) {
return response.sendStatus(400);
}
const fullpath = path.join(directory, filename);
fs.writeFileSync(fullpath, JSON.stringify(request.body.preset, null, 4), 'utf-8');
return response.send({ name });
});
app.post("/delete_preset", jsonParser, function (request, response) {
const name = sanitize(request.body.name);
if (!name) {
return response.sendStatus(400);
}
const filename = `${name}.settings`;
const directory = getPresetFolderByApiId(request.body.apiId);
if (!directory) {
return response.sendStatus(400);
}
const fullpath = path.join(directory, filename);
if (fs.existsSync) {
fs.unlinkSync(fullpath);
return response.sendStatus(200);
} else {
return response.sendStatus(404);
}
});
app.post("/savepreset_openai", jsonParser, function (request, response) {
const name = sanitize(request.query.name);
if (!request.body || !name) {
@ -3353,6 +3369,20 @@ app.post("/savepreset_openai", jsonParser, function (request, response) {
return response.send({ name });
});
function getPresetFolderByApiId(apiId) {
switch (apiId) {
case 'kobold':
case 'koboldhorde':
return directories.koboldAI_Settings;
case 'novel':
return directories.novelAI_Settings;
case 'textgenerationwebui':
return directories.textGen_Settings;
default:
return null;
}
}
function createTokenizationHandler(getTokenizerFn) {
return async function (request, response) {
if (!request.body) {
@ -3393,17 +3423,6 @@ app.post("/tokenize_via_api", jsonParser, async function (request, response) {
// ** REST CLIENT ASYNC WRAPPERS **
function putAsync(url, args) {
return new Promise((resolve, reject) => {
client.put(url, args, (data, response) => {
if (response.statusCode >= 400) {
reject(data);
}
resolve(data);
}).on('error', e => reject(e));
})
}
async function postAsync(url, args) {
const fetch = require('node-fetch').default;
const response = await fetch(url, { method: 'POST', timeout: 0, ...args });