diff --git a/.github/readme-zh_cn.md b/.github/readme-zh_cn.md new file mode 100644 index 000000000..e3ff35cc7 --- /dev/null +++ b/.github/readme-zh_cn.md @@ -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 编写的指南:** + + + +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 等模型): +* [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 安装(推荐使用,便于更新) + +附有精美图片示例的简易指南: + + + 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 code(zip)")。 + 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 地址: + +* Windows:Windows 按钮 > 在搜索栏中输入 `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% 原创内容的政策,因此旧的背景图片已从该资源库中删除。 + +不过你可以在这里找到它们的存档: + + + +## 屏幕截图 + +image +image + +## 许可证和贡献 + +** 发布本程序是希望它能有所帮助,但不做任何保证;甚至没有明示的性能、稳定性和其他任何特定用途的可用性保证。更多详情,请参阅 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 \ No newline at end of file diff --git a/.github/workflows/build-and-publish-release-dev.yml b/.github/workflows/build-and-publish-release.yml similarity index 82% rename from .github/workflows/build-and-publish-release-dev.yml rename to .github/workflows/build-and-publish-release.yml index 0c740892b..308a3cbd5 100644 --- a/.github/workflows/build-and-publish-release-dev.yml +++ b/.github/workflows/build-and-publish-release.yml @@ -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 }} diff --git a/.github/workflows/build-and-publish-release-main.yml b/.github/workflows/build-and-publish-staging.yml similarity index 82% rename from .github/workflows/build-and-publish-release-main.yml rename to .github/workflows/build-and-publish-staging.yml index ac8eeb03b..3e9b5783e 100644 --- a/.github/workflows/build-and-publish-release-main.yml +++ b/.github/workflows/build-and-publish-staging.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 635f8d346..fed4e2439 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/Dockerfile b/Dockerfile index 1bfe92284..100aed121 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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", "--" ] diff --git a/default/content/default_CodingSensei.png b/default/content/default_CodingSensei.png index 5d224fefc..e95d2347d 100644 Binary files a/default/content/default_CodingSensei.png and b/default/content/default_CodingSensei.png differ diff --git a/package-lock.json b/package-lock.json index c091c2615..a803cc4a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c5761758f..05dcec7bf 100644 --- a/package.json +++ b/package.json @@ -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 ." diff --git a/public/KoboldAI Settings/RecoveredRuins.settings b/public/KoboldAI Settings/RecoveredRuins.settings index 72f058659..13ab73370 100644 --- a/public/KoboldAI Settings/RecoveredRuins.settings +++ b/public/KoboldAI Settings/RecoveredRuins.settings @@ -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 ] -} \ No newline at end of file +} diff --git a/public/NovelAI Settings/Asper-Kayra.settings b/public/NovelAI Settings/Asper-Kayra.settings new file mode 100644 index 000000000..87e0777e7 --- /dev/null +++ b/public/NovelAI Settings/Asper-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Blended-Coffee-Kayra.settings b/public/NovelAI Settings/Blended-Coffee-Kayra.settings new file mode 100644 index 000000000..9f7dde19b --- /dev/null +++ b/public/NovelAI Settings/Blended-Coffee-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Blook-Kayra.settings b/public/NovelAI Settings/Blook-Kayra.settings new file mode 100644 index 000000000..c30902b55 --- /dev/null +++ b/public/NovelAI Settings/Blook-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Carefree-Kayra.settings b/public/NovelAI Settings/Carefree-Kayra.settings new file mode 100644 index 000000000..df71c09ab --- /dev/null +++ b/public/NovelAI Settings/Carefree-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Chat-Clio.settings b/public/NovelAI Settings/Chat-Clio.settings index cc9be1bfc..e79b6fe69 100644 --- a/public/NovelAI Settings/Chat-Clio.settings +++ b/public/NovelAI Settings/Chat-Clio.settings @@ -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 } diff --git a/public/NovelAI Settings/Edgewise-Clio.settings b/public/NovelAI Settings/Edgewise-Clio.settings new file mode 100644 index 000000000..03d6c8dea --- /dev/null +++ b/public/NovelAI Settings/Edgewise-Clio.settings @@ -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 +} diff --git a/public/NovelAI Settings/Fresh-Coffee-Clio.settings b/public/NovelAI Settings/Fresh-Coffee-Clio.settings index 7d12fd1d1..92791e18e 100644 --- a/public/NovelAI Settings/Fresh-Coffee-Clio.settings +++ b/public/NovelAI Settings/Fresh-Coffee-Clio.settings @@ -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 -} \ No newline at end of file +} diff --git a/public/NovelAI Settings/Fresh-Coffee-Kayra.settings b/public/NovelAI Settings/Fresh-Coffee-Kayra.settings new file mode 100644 index 000000000..edd4f3f36 --- /dev/null +++ b/public/NovelAI Settings/Fresh-Coffee-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Green-Active-Writer-Kayra.settings b/public/NovelAI Settings/Green-Active-Writer-Kayra.settings new file mode 100644 index 000000000..621acab5c --- /dev/null +++ b/public/NovelAI Settings/Green-Active-Writer-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Keelback-Clio.settings b/public/NovelAI Settings/Keelback-Clio.settings index 721cc25ed..f0e9d81ed 100644 --- a/public/NovelAI Settings/Keelback-Clio.settings +++ b/public/NovelAI Settings/Keelback-Clio.settings @@ -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 -} \ No newline at end of file +} diff --git a/public/NovelAI Settings/Long-Press-Clio.settings b/public/NovelAI Settings/Long-Press-Clio.settings index fdeecdcf0..5d9161527 100644 --- a/public/NovelAI Settings/Long-Press-Clio.settings +++ b/public/NovelAI Settings/Long-Press-Clio.settings @@ -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 -} \ No newline at end of file +} diff --git a/public/NovelAI Settings/Pilotfish-Kayra.settings b/public/NovelAI Settings/Pilotfish-Kayra.settings new file mode 100644 index 000000000..44d1ef269 --- /dev/null +++ b/public/NovelAI Settings/Pilotfish-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Stelenes-Kayra.settings b/public/NovelAI Settings/Stelenes-Kayra.settings new file mode 100644 index 000000000..796c28904 --- /dev/null +++ b/public/NovelAI Settings/Stelenes-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Talker-Chat-Clio.settings b/public/NovelAI Settings/Talker-Chat-Clio.settings index 6faad1414..fc6e2caeb 100644 --- a/public/NovelAI Settings/Talker-Chat-Clio.settings +++ b/public/NovelAI Settings/Talker-Chat-Clio.settings @@ -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 -} \ No newline at end of file +} diff --git a/public/NovelAI Settings/Tesseract-Kayra.settings b/public/NovelAI Settings/Tesseract-Kayra.settings new file mode 100644 index 000000000..0aaa4dc71 --- /dev/null +++ b/public/NovelAI Settings/Tesseract-Kayra.settings @@ -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 +} diff --git a/public/NovelAI Settings/Vingt-Un-Clio.settings b/public/NovelAI Settings/Vingt-Un-Clio.settings index 2631abf85..3cc01d03d 100644 --- a/public/NovelAI Settings/Vingt-Un-Clio.settings +++ b/public/NovelAI Settings/Vingt-Un-Clio.settings @@ -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 -} \ No newline at end of file +} diff --git a/public/NovelAI Settings/Writers-Daemon-Kayra.settings b/public/NovelAI Settings/Writers-Daemon-Kayra.settings new file mode 100644 index 000000000..9b1c4e4b7 --- /dev/null +++ b/public/NovelAI Settings/Writers-Daemon-Kayra.settings @@ -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 +} diff --git a/public/index.html b/public/index.html index bc8d85fd3..913a0c55f 100644 --- a/public/index.html +++ b/public/index.html @@ -95,6 +95,7 @@ + SillyTavern @@ -131,15 +132,24 @@
+

Kobold Presets ?

- + +
+ + + + + + +

@@ -148,7 +158,7 @@ ?

-
@@ -171,8 +181,15 @@

Text Gen WebUI (ooba) presets

- +
+ + + + + + +

@@ -528,6 +545,19 @@
+
+
+ Proxy Password +
+
+ + Will be used as a password for the proxy instead of API key.
+
+
+
+ +
+
+
+
+ Top P +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Top A +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Top K +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Typical P +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ CFG Scale +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Phrase Repetition Penalty +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Min Length +
+
+
+ +
+
+
+ select +
+
+
+
@@ -1387,6 +1522,7 @@ +
@@ -1458,6 +1594,11 @@
+
+ + Use "Proxy password" field instead. This input will be ignored. + +
For privacy reasons, your API key will be hidden after you reload the page.
@@ -1775,8 +1916,8 @@ - - + +
@@ -2280,6 +2421,10 @@ Swipes +
@@ -2780,6 +2925,8 @@ + + @@ -3300,7 +3447,8 @@
- + +
diff --git a/public/instruct/Llama2.json b/public/instruct/Llama2.json index b62802cc5..5b23b71b8 100644 --- a/public/instruct/Llama2.json +++ b/public/instruct/Llama2.json @@ -1,6 +1,6 @@ { "name": "Llama 2", - "system_prompt": "Write {{user}}'s next reply in this fictional roleplay with {{char}}.\n<>\n", + "system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.\n<>\n", "system_sequence": "[INST] <>\n", "stop_sequence": "", "input_sequence": "[INST]", diff --git a/public/script.js b/public/script.js index cef4028d1..157d79a99 100644 --- a/public/script.js +++ b/public/script.js @@ -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 ') //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(` +

Are you sure you want to duplicate this character?

+ If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.

`, + '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( `` ); - $('#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 () { diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index cb563bbf9..7ee3d333d 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -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) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index dfc45176f..ca0548f32 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -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 = []; diff --git a/public/scripts/extensions/infinity-context/index.js b/public/scripts/extensions/infinity-context/index.js index 337ab4662..47fa0d04f 100644 --- a/public/scripts/extensions/infinity-context/index.js +++ b/public/scripts/extensions/infinity-context/index.js @@ -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 () => { - + - - + + Memory Recall Strategy - + diff --git a/public/scripts/extensions/objective/index.js b/public/scripts/extensions/objective/index.js index 3485da1ec..21739b6f8 100644 --- a/public/scripts/extensions/objective/index.js +++ b/public/scripts/extensions/objective/index.js @@ -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 = `
- ${this.description} + ${this.description}
+

`; @@ -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 += ` +
+
+ + + + + + +
+
+ + +
+
+ + + +
+
` + 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() { `) $("#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(() => {
- Objective -
-
-
- - -
- - + Objective +
- -
-
-
- - +
+ + +
+ +
-
-
- - - - (0 = disabled) +
+ + Go to parent task
+ +
+
+
+ + +
+
+
+ + + + (0 = disabled) +
+
+ Messages until next AI task completion check 0 +
+ +
+
- Messages until next AI task completion check 0 -
-
`; +
+ `; 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, () => { diff --git a/public/scripts/extensions/objective/style.css b/public/scripts/extensions/objective/style.css index 987044604..cc20b1769 100644 --- a/public/scripts/extensions/objective/style.css +++ b/public/scripts/extensions/objective/style.css @@ -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; } diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js index 610fe5e09..103f294a1 100644 --- a/public/scripts/extensions/regex/index.js +++ b/public/scripts/extensions/regex/index.js @@ -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); } diff --git a/public/scripts/extensions/speech-recognition/browser.js b/public/scripts/extensions/speech-recognition/browser.js new file mode 100644 index 000000000..f51019894 --- /dev/null +++ b/public/scripts/extensions/speech-recognition/browser.js @@ -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 = " " + +class BrowserSttProvider { + //########// + // Config // + //########// + + settings = { + language: "" + } + + defaultSettings = { + language: "en-US", + } + + processTranscriptFunction = null; + + get settingsHtml() { + let html = ' \ + Language
\ + \ + ' + 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") + } + + +} diff --git a/public/scripts/extensions/speech-recognition/index.js b/public/scripts/extensions/speech-recognition/index.js index b306bf690..e32a9a3db 100644 --- a/public/scripts/extensions/speech-recognition/index.js +++ b/public/scripts/extensions/speech-recognition/index.js @@ -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 = " " + +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(": 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 = $('
'); +$(document).ready(function () { + function addExtensionControls() { + const settingsHtml = ` +
+
+
+ Speech Recognition +
+
+
+
+ Select Speech-to-text Provider
+ +
+
+ Message Mode
+ +
+
+ Message Mapping + + + +
+
+
+
+
+
+ `; + $('#extensions_settings').append(settingsHtml); + $('#speech_recognition_provider_settings').on('input', onSttProviderSettingsInput); + for (const provider in sttProviders) { + $('#speech_recognition_provider').append($("