mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Compare commits
312 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9b5e082a26 | ||
|
0ed340bd9a | ||
|
26a9c2889f | ||
|
53e41bdda8 | ||
|
25bd2541f7 | ||
|
6c909acea6 | ||
|
8c70b1decd | ||
|
7effc3497d | ||
|
cd86999d30 | ||
|
2fea218661 | ||
|
ef3a9a810e | ||
|
2c3cfb31f5 | ||
|
7c6429a577 | ||
|
e343f2461d | ||
|
c94eae6eb6 | ||
|
9a7654598e | ||
|
5ac375097b | ||
|
508e1a06da | ||
|
5ba7588838 | ||
|
98f8613e38 | ||
|
5ea30d9d4d | ||
|
78825352e5 | ||
|
264b52b52d | ||
|
0cb63e689d | ||
|
c1ab0212e5 | ||
|
128945aaaa | ||
|
75bb0d641f | ||
|
fcc51b6481 | ||
|
c124fc589f | ||
|
32e5566a37 | ||
|
f8e8929834 | ||
|
4167047f9b | ||
|
5f97a52d58 | ||
|
7384cb07a4 | ||
|
e0d6430ade | ||
|
23a57e86a7 | ||
|
6e55b99aa9 | ||
|
2424367460 | ||
|
c4602df108 | ||
|
c86e2154c9 | ||
|
151f4d322c | ||
|
04a2d82a8d | ||
|
cfc4394e41 | ||
|
74b973c571 | ||
|
ef1f6b3143 | ||
|
58f7c77281 | ||
|
39e43e8a0d | ||
|
b59e0cdfc8 | ||
|
e3f48ea767 | ||
|
b3bb88e99b | ||
|
51ed6ba0c9 | ||
|
632d1a2a17 | ||
|
078eb051ad | ||
|
5b1095abcb | ||
|
b1666dc18d | ||
|
3aa8229401 | ||
|
734b0201be | ||
|
6ddfcc7a06 | ||
|
ef8aef7994 | ||
|
8128c2436b | ||
|
649bc69b19 | ||
|
bfac10028f | ||
|
4ae3e9db0a | ||
|
d05d4042fe | ||
|
1061971cdb | ||
|
6b2455da2b | ||
|
7b5bb030ad | ||
|
19b3e13675 | ||
|
d16f6f8fea | ||
|
45a66fae9d | ||
|
6b288ca83c | ||
|
d59d7e6c8d | ||
|
8b3f65073c | ||
|
b407fe2388 | ||
|
33af7ad266 | ||
|
cd8a24a712 | ||
|
84283bc2b4 | ||
|
14827d6135 | ||
|
b34df90905 | ||
|
c18a845f64 | ||
|
2a00e98ec5 | ||
|
d84d55c35f | ||
|
853d81e67c | ||
|
791b18d78e | ||
|
079b1623c5 | ||
|
708b065300 | ||
|
140b86d822 | ||
|
8f1321f09d | ||
|
9333340175 | ||
|
4698f0f765 | ||
|
f106666ded | ||
|
2d07cce1dd | ||
|
ea809023b5 | ||
|
bcffaec6a6 | ||
|
008d8fa6fc | ||
|
926bf0b4c9 | ||
|
aefafdebe0 | ||
|
2f94451948 | ||
|
e5a678fb2d | ||
|
109f6ace63 | ||
|
44340208de | ||
|
19a0f8e5ac | ||
|
2a153e3b15 | ||
|
249ab7106a | ||
|
d5e32af9b2 | ||
|
65732f4406 | ||
|
3da9438b63 | ||
|
f892931d44 | ||
|
a837b1e2fb | ||
|
9cf4056b28 | ||
|
37e653dcf4 | ||
|
0a12fe0bdb | ||
|
56a4a6eb83 | ||
|
8446f10408 | ||
|
4b7c837fe3 | ||
|
aebdd6cd42 | ||
|
73f15060c9 | ||
|
e381e1cefc | ||
|
2fc6813e66 | ||
|
0ad3c86e17 | ||
|
9f44a72d76 | ||
|
c760447288 | ||
|
ea0fe349cd | ||
|
1f56f0d64a | ||
|
905131c764 | ||
|
31feaee805 | ||
|
a07cbe5f7f | ||
|
67fa7b9607 | ||
|
1b005ef47f | ||
|
c8b5b7da22 | ||
|
5a67d72fea | ||
|
68e5ae63d6 | ||
|
9712e4bbb0 | ||
|
61c0e3b08b | ||
|
d4278388f7 | ||
|
2fdec7eb03 | ||
|
1d0f67c144 | ||
|
14ef5d9a6b | ||
|
143b4347c2 | ||
|
2a08e199d2 | ||
|
f198f5eb6e | ||
|
65a16970f4 | ||
|
5a7c4947b3 | ||
|
bb3fc5be62 | ||
|
ea800d1550 | ||
|
90e08e08de | ||
|
9d99b89c9c | ||
|
baddee8082 | ||
|
a51653e8b5 | ||
|
7dfaf6f0b0 | ||
|
3f015f4bd2 | ||
|
6f18c457fc | ||
|
c57de3d47b | ||
|
cd02abe205 | ||
|
aac7525204 | ||
|
42cc66f06e | ||
|
f6f51d21c5 | ||
|
9a4d62ca6f | ||
|
9d023dc3b1 | ||
|
ac98ebcc6c | ||
|
29a3c5d590 | ||
|
0c0e24323c | ||
|
32f605e413 | ||
|
af8c21fea2 | ||
|
72974d8a54 | ||
|
7f86551ab4 | ||
|
c5d87e4808 | ||
|
e5f3a70860 | ||
|
7596d78322 | ||
|
5645432e9d | ||
|
bad7892baa | ||
|
a0c8ac54dd | ||
|
73fd306b8b | ||
|
72213add56 | ||
|
6f4fd15095 | ||
|
78d62d7be2 | ||
|
99af6ed472 | ||
|
16b45f1ea9 | ||
|
435d319090 | ||
|
e7148c41a9 | ||
|
192c82b180 | ||
|
8aff89de30 | ||
|
6768c56e2b | ||
|
4939387bbf | ||
|
4c14b8ee2d | ||
|
0bbcf0db83 | ||
|
29d841a50b | ||
|
0c919bf32d | ||
|
9f92b19004 | ||
|
7824a18103 | ||
|
18e6e578dd | ||
|
0eef05908d | ||
|
d5e40e0271 | ||
|
6b85f10818 | ||
|
73ea029acd | ||
|
27c6e5ecff | ||
|
40f466b2c3 | ||
|
02667d3d1a | ||
|
9cb70817c7 | ||
|
e172f50d4f | ||
|
49f7185c8c | ||
|
638050a3de | ||
|
a0f369d100 | ||
|
f9239c860d | ||
|
41a08fed2b | ||
|
95d43712d3 | ||
|
4f76a8b0ce | ||
|
3fc2b81433 | ||
|
23a514bba8 | ||
|
51449c4913 | ||
|
18bb362578 | ||
|
6d9c2ec365 | ||
|
ad26a1968b | ||
|
52289acc62 | ||
|
b970c69844 | ||
|
c8e456d773 | ||
|
d00fbacec3 | ||
|
ccbec7715a | ||
|
1b7973ec13 | ||
|
1f63fd9344 | ||
|
d8d5118283 | ||
|
11226ff7d6 | ||
|
844d9c86a4 | ||
|
0aee97ccca | ||
|
fa06e5ee5a | ||
|
6794be4d1e | ||
|
4316396880 | ||
|
fd93bc7736 | ||
|
0cc88c17c7 | ||
|
6829f5308f | ||
|
1689913b43 | ||
|
33b7e8fad1 | ||
|
17eb55731e | ||
|
9dba57dc85 | ||
|
dddc49c235 | ||
|
faf9c4445f | ||
|
a646aaa125 | ||
|
80a0cb2bc4 | ||
|
675cabb7e3 | ||
|
f36cf088a1 | ||
|
df8d7075a8 | ||
|
314b194891 | ||
|
ac4fa0e035 | ||
|
0e7c10b26f | ||
|
64914e778d | ||
|
763a443680 | ||
|
27eb95cb36 | ||
|
732707ae2b | ||
|
ee81a8d88c | ||
|
b107848ccb | ||
|
ec270378d4 | ||
|
7c3b598eea | ||
|
aed0dab95d | ||
|
311249c5e0 | ||
|
88f3e01f72 | ||
|
8e06d18664 | ||
|
3fb4756c03 | ||
|
707ce62017 | ||
|
ddf485f354 | ||
|
c3f61af18a | ||
|
3eea8a714c | ||
|
49186dbfc1 | ||
|
87b9da11c6 | ||
|
77124056b8 | ||
|
8d794ed03f | ||
|
d4e05c99e3 | ||
|
111b91b1ed | ||
|
bed6b2d777 | ||
|
99a18be5fc | ||
|
ccf5454750 | ||
|
62e5f71cf9 | ||
|
f56834bb96 | ||
|
68819a1afc | ||
|
e4e2ff20a2 | ||
|
5c66c34414 | ||
|
1055673865 | ||
|
7a4b8abde7 | ||
|
1ad361593f | ||
|
ae850fdde7 | ||
|
c8b3bce8b9 | ||
|
c3d43a7d05 | ||
|
40f7aa47ad | ||
|
0e45aa7e58 | ||
|
ce3d76d662 | ||
|
c4209b9448 | ||
|
83cb3456fc | ||
|
e15c739e64 | ||
|
0df8c4b6a2 | ||
|
8fdea22379 | ||
|
260bb0af52 | ||
|
cf3baccc5e | ||
|
1bc9bdd64f | ||
|
b2dc4c6c1b | ||
|
130559d499 | ||
|
06899581db | ||
|
e9288874b1 | ||
|
b6243cdbe1 | ||
|
ed86e8de04 | ||
|
1b9ca4c9d9 | ||
|
dd69fd3934 | ||
|
3f47386cba | ||
|
83252617be | ||
|
a9f8506218 | ||
|
ea68e70ac7 | ||
|
e38a9df538 | ||
|
d3fe20c119 | ||
|
3feffaabe5 | ||
|
52b2c89d64 | ||
|
59100a0076 | ||
|
80fad96b1f | ||
|
eccfadb14e | ||
|
01f1f67de3 |
@@ -5,3 +5,4 @@ readme*
|
||||
Start.bat
|
||||
/dist
|
||||
/backups/
|
||||
cloudflared.exe
|
||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,7 +7,7 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> **Warning**. Complete **all** the fields below. Otherwise your bug report will be **ignored**!
|
||||
> **Warning**. Complete **all** the fields below. Otherwise, your bug report will be **ignored**!
|
||||
|
||||
**Have you searched for similar [bugs](https://github.com/SillyTavern/SillyTavern/issues?q=)?**
|
||||
Yes/No
|
||||
@@ -38,7 +38,7 @@ Providing the logs from the browser DevTools console (opened by pressing the F12
|
||||
- Node.js version (if applicable): [run `node --version` in cmd]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Generation API [e.g. KoboldAI, OpenAI]
|
||||
- Branch [main, dev]
|
||||
- Branch [staging, release]
|
||||
- Model [e.g. Pygmalion 6b, LLaMa 13b]
|
||||
|
||||
**Additional context**
|
||||
|
309
.github/readme-zh_cn.md
vendored
Normal file
309
.github/readme-zh_cn.md
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
[English](readme.md) | 中文
|
||||
|
||||

|
||||
|
||||
移动设备界面友好,多种人工智能服务或模型支持(KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale),类似 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 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% 原创内容的政策,因此旧的背景图片已从该资源库中删除。
|
||||
|
||||
不过你可以在这里找到它们的存档:
|
||||
|
||||
<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>)
|
||||
* StefanDanielSchwarz's various commits and bug reports (<https://github.com/StefanDanielSchwarz>)
|
||||
* 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
|
||||
* 中文翻译由 [@XXpE3](https://github.com/XXpE3) 完成,中文 ISSUES 可以联系 @XXpE3
|
10
.github/readme.md
vendored
10
.github/readme.md
vendored
@@ -1,6 +1,8 @@
|
||||
English | [中文](readme-zh_cn.md)
|
||||
|
||||

|
||||
|
||||
Mobile-friendly, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI+proxies, WindowAI(Claude!)), VN-like Waifu Mode, Horde SD, System TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need. Optional Extras server for more SD/TTS options + ChromaDB/Summarize.
|
||||
Mobile-friendly, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale), VN-like Waifu Mode, Horde SD, System TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need. Optional Extras server for more SD/TTS options + ChromaDB/Summarize.
|
||||
|
||||
Based on a fork of TavernAI 1.2.8
|
||||
|
||||
@@ -65,7 +67,7 @@ Get in touch with the developers directly:
|
||||
* Chat bookmarks / branching (duplicates the dialogue in its current state)
|
||||
* Advanced KoboldAI / TextGen generation settings with a lot of community-made presets
|
||||
* World Info support: create rich lore or save tokens on your character card
|
||||
* Window AI browser extension support (run models like Claude, GPT 4): <https://windowai.io/>
|
||||
* [OpenRouter](https://openrouter.ai) connection for various APIs (Claude, GPT-4/3.5 and more)
|
||||
* [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui) API connection
|
||||
* [AI Horde](https://horde.koboldai.net/) connection
|
||||
* Prompt generation formatting tweaking
|
||||
@@ -293,7 +295,7 @@ GNU Affero General Public License for more details.**
|
||||
* 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>)
|
||||
* StefanDanielSchwarz's various commits and bug reports (<https://github.com/StefanDanielSchwarz>)
|
||||
* 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
|
||||
@@ -306,3 +308,5 @@ GNU Affero General Public License for more details.**
|
||||
* 10K Discord Users Celebratory Background by @kallmeflocc
|
||||
* Default content (characters and lore books) provided by @OtisAlejandro, @RossAscends and @kallmeflocc
|
||||
* Korean translation by @doloroushyeonse
|
||||
* k_euler_a support for Horde by <https://github.com/Teashrock>
|
||||
* Chinese translation by [@XXpE3](https://github.com/XXpE3), 中文 ISSUES 可以联系 @XXpE3
|
||||
|
@@ -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 }}
|
@@ -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 }}
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -9,12 +9,14 @@ 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/
|
||||
*.jsonl
|
||||
/config.conf
|
||||
/docker/config.conf
|
||||
/docker/config
|
||||
.DS_Store
|
||||
public/settings.json
|
||||
/thumbnails
|
||||
@@ -24,4 +26,6 @@ secrets.json
|
||||
/dist
|
||||
/backups/
|
||||
public/movingUI/
|
||||
public/QuickReplies/
|
||||
content.log
|
||||
cloudflared.exe
|
||||
|
@@ -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", "--" ]
|
||||
@@ -23,13 +23,17 @@ COPY . ./
|
||||
|
||||
# Copy default chats, characters and user avatars to <folder>.default folder
|
||||
RUN \
|
||||
IFS="," RESOURCES="characters,chats,groups,group chats,User Avatars,worlds,settings.json" && \
|
||||
IFS="," RESOURCES="characters,chats,groups,group chats,User Avatars,worlds" && \
|
||||
\
|
||||
echo "*** Store default $RESOURCES in <folder>.default ***" && \
|
||||
for R in $RESOURCES; do mv "public/$R" "public/$R.default"; done && \
|
||||
\
|
||||
echo "*** Create symbolic links to config directory ***" && \
|
||||
for R in $RESOURCES; do ln -s "../config/$R" "public/$R"; done && \
|
||||
# rm "config.conf" "public/settings.json" "public/css/bg_load.css" && \
|
||||
ln -s "./config/config.conf" "config.conf" && \
|
||||
ln -s "../config/settings.json" "public/settings.json" && \
|
||||
ln -s "../../config/bg_load.css" "public/css/bg_load.css" && \
|
||||
mkdir "config"
|
||||
|
||||
# Cleanup unnecessary files
|
||||
|
18
Remote-Link.cmd
Normal file
18
Remote-Link.cmd
Normal file
@@ -0,0 +1,18 @@
|
||||
@echo off
|
||||
echo ========================================================================================================================
|
||||
echo WARNING: Cloudflare Tunnel!
|
||||
echo ========================================================================================================================
|
||||
echo This script downloads and runs the latest cloudflared.exe from Cloudflare to set up an HTTPS tunnel to your SillyTavern!
|
||||
echo Using the randomly generated temporary tunnel URL, anyone can access your SillyTavern over the Internet while the tunnel
|
||||
echo is active. Keep the URL safe and secure your SillyTavern installation by setting a username and password in config.conf!
|
||||
echo.
|
||||
echo See https://docs.sillytavern.app/usage/remoteconnections/ for more details about how to secure your SillyTavern install.
|
||||
echo.
|
||||
echo By continuing you confirm that you're aware of the potential dangers of having a tunnel open and take all responsibility
|
||||
echo to properly use and secure it!
|
||||
echo.
|
||||
echo To abort, press Ctrl+C or close this window now!
|
||||
echo.
|
||||
pause
|
||||
if not exist cloudflared.exe curl -Lo cloudflared.exe https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe
|
||||
cloudflared.exe tunnel --url localhost:8000
|
Binary file not shown.
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 338 KiB |
@@ -14,6 +14,7 @@
|
||||
"world_info_depth": 2,
|
||||
"world_info_budget": 25,
|
||||
"world_info_recursive": true,
|
||||
"world_info_overflow_alert": false,
|
||||
"world_info_case_sensitive": false,
|
||||
"world_info_match_whole_words": false,
|
||||
"world_info_character_strategy": 1,
|
||||
@@ -27,6 +28,7 @@
|
||||
"eta_cutoff": 0,
|
||||
"typical_p": 1,
|
||||
"rep_pen": 1.1,
|
||||
"rep_pen_range": 0,
|
||||
"no_repeat_ngram_size": 0,
|
||||
"penalty_alpha": 0,
|
||||
"num_beams": 1,
|
||||
@@ -57,7 +59,7 @@
|
||||
"trusted_workers_only": false
|
||||
},
|
||||
"power_user": {
|
||||
"tokenizer": 3,
|
||||
"tokenizer": 99,
|
||||
"token_padding": 64,
|
||||
"collapse_newlines": false,
|
||||
"pygmalion_formatting": 0,
|
||||
@@ -91,7 +93,7 @@
|
||||
"sort_order": "asc",
|
||||
"sort_rule": null,
|
||||
"font_scale": 1,
|
||||
"blur_strength": 5,
|
||||
"blur_strength": 10,
|
||||
"shadow_width": 2,
|
||||
"main_text_color": "rgba(220, 220, 210, 1)",
|
||||
"italics_text_color": "rgba(145, 145, 145, 1)",
|
||||
@@ -144,7 +146,9 @@
|
||||
"persona_descriptions": {},
|
||||
"persona_description": "",
|
||||
"persona_description_position": 0,
|
||||
"custom_stopping_strings": ""
|
||||
"custom_stopping_strings": "",
|
||||
"custom_stopping_strings_macro": true,
|
||||
"fuzzy_search": false
|
||||
},
|
||||
"extension_settings": {
|
||||
"apiUrl": "http://localhost:5100",
|
||||
@@ -183,7 +187,14 @@
|
||||
"promptInterval": 10,
|
||||
"promptMinInterval": 1,
|
||||
"promptMaxInterval": 100,
|
||||
"promptIntervalStep": 1
|
||||
"promptIntervalStep": 1,
|
||||
"template": "[Summary: {{summary}}]",
|
||||
"position": 0,
|
||||
"depth": 2,
|
||||
"promptForceWords": 0,
|
||||
"promptForceWordsStep": 100,
|
||||
"promptMinForceWords": 0,
|
||||
"promptMaxForceWords": 10000
|
||||
},
|
||||
"note": {
|
||||
"default": "",
|
||||
@@ -203,7 +214,8 @@
|
||||
"ttsEnabled": false,
|
||||
"currentProvider": "System",
|
||||
"auto_generation": true,
|
||||
"ElevenLabs": {}
|
||||
"ElevenLabs": {},
|
||||
"System": {}
|
||||
},
|
||||
"sd": {
|
||||
"scale_min": 1,
|
||||
@@ -228,7 +240,16 @@
|
||||
"horde": true,
|
||||
"horde_nsfw": false,
|
||||
"horde_karras": true,
|
||||
"refine_mode": false
|
||||
"refine_mode": false,
|
||||
"prompts": {
|
||||
"0": "[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,']",
|
||||
"1": "[Pause your roleplay and provide a detailed description of {{user}}'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. 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,'. Ignore the rest of the story when crafting this description. Do not roleplay as {{char}} when writing this description, and do not attempt to continue the story.]",
|
||||
"2": "[Pause your roleplay and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}'s appearance, and {{char}}'s surroundings. Do not roleplay while writing this description.]",
|
||||
"3": "[Pause your roleplay and provide ONLY the last chat message string back to me verbatim. Do not write anything after the string. Do not roleplay at all in your response. Do not continue the roleplay story.]",
|
||||
"4": "[Pause your roleplay. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message.\n\n Only mention characters by using pronouns ('he','his','she','her','it','its') or neutral nouns ('male', 'the man', 'female', 'the woman').\n\n Ignore non-visible things such as feelings, personality traits, thoughts, and spoken dialog.\n\n Add keywords in this precise order:\n a keyword to describe the location of the scene,\n a keyword to mention how many characters of each gender or type are present in the scene (minimum of two characters:\n {{user}} and {{char}}, example: '2 men ' or '1 man 1 woman ', '1 man 3 robots'),\n\n keywords to describe the relative physical positioning of the characters to each other (if a commonly known term for the positioning is known use it instead of describing the positioning in detail) + 'POV',\n\n a single keyword or phrase to describe the primary act taking place in the last chat message,\n\n keywords to describe {{char}}'s physical appearance and facial expression,\n keywords to describe {{char}}'s actions,\n keywords to describe {{user}}'s physical appearance and actions.\n\n If character actions involve direct physical interaction with another character, mention specifically which body parts interacting and how.\n\n A correctly formatted example response would be:\n '(location),(character list by gender),(primary action), (relative character position) POV, (character 1's description and actions), (character 2's description and actions)']",
|
||||
"5": "[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, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. 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 'close up facial portrait,']"
|
||||
},
|
||||
"character_prompts": {}
|
||||
},
|
||||
"chromadb": {},
|
||||
"translate": {
|
||||
@@ -237,7 +258,15 @@
|
||||
"provider": "google",
|
||||
"auto_mode": "none"
|
||||
},
|
||||
"objective": {},
|
||||
"objective": {
|
||||
"customPrompts": {
|
||||
"default": {
|
||||
"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.\n\nThe objective that you must make a numbered task list for is: [{{objective}}].\nThe 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.\n\nGiven an example objective of 'Make me a four course dinner', here is an example output:\n1. Determine what the courses will be\n2. Find recipes for each course\n3. Go shopping for supplies with {{user}}\n4. Cook the food\n5. Get {{user}} to set the table\n6. Serve the food\n7. Enjoy eating the meal with {{user}}\n ",
|
||||
"checkTaskCompleted": "Pause your roleplay. Determine if this task is completed: [{{task}}].\nTo do this, examine the most recent messages. Your response must only contain either true or false, nothing other words.\nExample output:\ntrue\n ",
|
||||
"currentTask": "Your current task is [{{task}}]. Balance existing roleplay with completing this task."
|
||||
}
|
||||
}
|
||||
},
|
||||
"quickReply": {
|
||||
"quickReplyEnabled": false,
|
||||
"numberOfSlots": 5,
|
||||
@@ -273,6 +302,14 @@
|
||||
"controls": [],
|
||||
"fluctuation": 0.1,
|
||||
"enabled": false
|
||||
},
|
||||
"speech_recognition": {
|
||||
"currentProvider": "None",
|
||||
"messageMode": "append",
|
||||
"messageMappingText": "",
|
||||
"messageMapping": [],
|
||||
"messageMappingEnabled": false,
|
||||
"None": {}
|
||||
}
|
||||
},
|
||||
"context_settings": {
|
||||
@@ -296,33 +333,46 @@
|
||||
"1345561466591"
|
||||
]
|
||||
},
|
||||
"temp_novel": 1.11,
|
||||
"rep_pen_novel": 1.11,
|
||||
"rep_pen_size_novel": 320,
|
||||
"model_novel": "euterpe-v2",
|
||||
"preset_settings_novel": "Classic-Euterpe",
|
||||
"streaming_novel": false,
|
||||
"temp": 1,
|
||||
"rep_pen": 1.1,
|
||||
"rep_pen_range": 600,
|
||||
"top_p": 0.95,
|
||||
"top_a": 0,
|
||||
"top_k": 0,
|
||||
"typical": 1,
|
||||
"tfs": 1,
|
||||
"rep_pen_slope": 0,
|
||||
"single_line": false,
|
||||
"use_stop_sequence": false,
|
||||
"streaming_kobold": false,
|
||||
"sampler_order": [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"nai_settings": {
|
||||
"temperature": 0.63,
|
||||
"repetition_penalty": 1.148125,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0.09,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"tail_free_sampling": 0.975,
|
||||
"top_k": 0,
|
||||
"top_p": 0.975,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"min_length": 1,
|
||||
"model_novel": "euterpe-v2",
|
||||
"preset_settings_novel": "Classic-Euterpe",
|
||||
"streaming_novel": false
|
||||
},
|
||||
"kai_settings": {
|
||||
"temp": 1,
|
||||
"rep_pen": 1.1,
|
||||
"rep_pen_range": 600,
|
||||
"top_p": 0.95,
|
||||
"top_a": 0,
|
||||
"top_k": 0,
|
||||
"typical": 1,
|
||||
"tfs": 1,
|
||||
"rep_pen_slope": 0,
|
||||
"single_line": false,
|
||||
"use_stop_sequence": false,
|
||||
"streaming_kobold": false,
|
||||
"sampler_order": [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]
|
||||
},
|
||||
"preset_settings_openai": "Default",
|
||||
"temp_openai": "0.9",
|
||||
"freq_pen_openai": 0.7,
|
||||
@@ -374,5 +424,8 @@
|
||||
"legacy_streaming": false,
|
||||
"chat_completion_source": "openai",
|
||||
"max_context_unlocked": false,
|
||||
"api_url_scale": ""
|
||||
"api_url_scale": "",
|
||||
"show_external_models": false,
|
||||
"proxy_password": "",
|
||||
"assistant_prefill": ""
|
||||
}
|
||||
|
@@ -9,5 +9,4 @@ services:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "./config:/home/node/app/config"
|
||||
- "./config.conf:/home/node/app/config.conf"
|
||||
restart: unless-stopped
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Initialize missing user files
|
||||
IFS="," RESOURCES="characters,groups,group chats,chats,User Avatars,worlds,settings.json"
|
||||
IFS="," RESOURCES="characters,groups,group chats,chats,User Avatars,worlds"
|
||||
for R in $RESOURCES; do
|
||||
if [ ! -e "config/$R" ]; then
|
||||
echo "Resource not found, copying from defaults: $R"
|
||||
@@ -9,5 +9,20 @@ for R in $RESOURCES; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -e "config/config.conf" ]; then
|
||||
echo "Resource not found, copying from defaults: config.conf"
|
||||
cp -r "default/config.conf" "config/config.conf"
|
||||
fi
|
||||
|
||||
if [ ! -e "config/settings.json" ]; then
|
||||
echo "Resource not found, copying from defaults: settings.json"
|
||||
cp -r "default/settings.json" "config/settings.json"
|
||||
fi
|
||||
|
||||
if [ ! -e "config/bg_load.css" ]; then
|
||||
echo "Resource not found, copying from defaults: bg_load.css"
|
||||
cp -r "default/bg_load.css" "config/bg_load.css"
|
||||
fi
|
||||
|
||||
# Start the server
|
||||
exec node server.js
|
||||
|
61
package-lock.json
generated
61
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sillytavern",
|
||||
"version": "1.9.2",
|
||||
"version": "1.9.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sillytavern",
|
||||
"version": "1.9.2",
|
||||
"version": "1.9.6",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
@@ -42,7 +42,6 @@
|
||||
"sentencepiece-js": "^1.1.0",
|
||||
"simple-git": "^3.19.1",
|
||||
"uniqolor": "^1.1.0",
|
||||
"user-agents": "^1.0.1444",
|
||||
"webp-converter": "2.3.2",
|
||||
"ws": "^8.13.0",
|
||||
"yargs": "^17.7.1",
|
||||
@@ -1223,14 +1222,6 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-indent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz",
|
||||
"integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
|
||||
@@ -1260,32 +1251,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/docopt": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/docopt/-/docopt-0.6.2.tgz",
|
||||
"integrity": "sha512-NqTbaYeE4gA/wU1hdKFdU+AFahpDOpgGLzHP42k6H6DKExJd0A55KEVWYhL9FEmHmgeLvEU2vuKXDuU+4yToOw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"node_modules/dot-json": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-json/-/dot-json-1.3.0.tgz",
|
||||
"integrity": "sha512-Pu11Prog/Yjf2lBICow82/DSV46n3a2XT1Rqt/CeuhkO1fuacF7xydYhI0SwQx2Ue0jCyLtQzgKPFEO6ewv+bQ==",
|
||||
"dependencies": {
|
||||
"detect-indent": "~6.0.0",
|
||||
"docopt": "~0.6.2",
|
||||
"underscore-keypath": "~0.0.22"
|
||||
},
|
||||
"bin": {
|
||||
"dot-json": "bin/dot-json.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -2037,11 +2007,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -3456,19 +3421,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
|
||||
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
|
||||
},
|
||||
"node_modules/underscore-keypath": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/underscore-keypath/-/underscore-keypath-0.0.22.tgz",
|
||||
"integrity": "sha512-fU7aYj1J2LQd+jqdQ67AlCOZKK3Pl+VErS8fGYcgZG75XB9/bY+RLM+F2xEcKHhHNtLvqqFyXAoZQlLYfec3Xg==",
|
||||
"dependencies": {
|
||||
"underscore": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/uniqolor": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uniqolor/-/uniqolor-1.1.0.tgz",
|
||||
@@ -3491,15 +3443,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/user-agents": {
|
||||
"version": "1.0.1444",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.1444.tgz",
|
||||
"integrity": "sha512-6WXJ0RZuUKgif1rW5FN02HnpoJ8EzH6COQoXCiVStZEVPz+YnAx3iA48etY3ZD4UwueYN9ALC7j4ayHvYEh7tA==",
|
||||
"dependencies": {
|
||||
"dot-json": "^1.3.0",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
|
@@ -33,7 +33,6 @@
|
||||
"sentencepiece-js": "^1.1.0",
|
||||
"simple-git": "^3.19.1",
|
||||
"uniqolor": "^1.1.0",
|
||||
"user-agents": "^1.0.1444",
|
||||
"webp-converter": "2.3.2",
|
||||
"ws": "^8.13.0",
|
||||
"yargs": "^17.7.1",
|
||||
@@ -51,9 +50,10 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.9.2",
|
||||
"version": "1.9.6",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start-multi": "node server.js --disableCsrf",
|
||||
"pkg": "pkg --compress Gzip --no-bytecode --public ."
|
||||
},
|
||||
"bin": {
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 1.15,
|
||||
"top_k": 0,
|
||||
"top_p": 0.95,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 90,
|
||||
"temp": 0.8,
|
||||
"top_k": 28,
|
||||
"top_p": 0.94,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.59,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.8,
|
||||
"top_k": 100,
|
||||
"top_p": 0.9,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 180,
|
||||
"temp": 1.0,
|
||||
"top_p": 0.9,
|
||||
"top_k": 40,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 180,
|
||||
"temp": 0.43,
|
||||
"top_p": 0.96,
|
||||
"top_k": 0,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 180,
|
||||
"temp": 0.65,
|
||||
"top_p": 0.9,
|
||||
"top_k": 0,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.51,
|
||||
"top_p": 1,
|
||||
"top_k": 0,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 1600,
|
||||
"genamt": 180,
|
||||
"temp": 0.79,
|
||||
"top_k": 0,
|
||||
"top_p": 0.9,
|
||||
|
22
public/KoboldAI Settings/Deterministic.settings
Normal file
22
public/KoboldAI Settings/Deterministic.settings
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"temp": 0,
|
||||
"rep_pen": 1.1,
|
||||
"rep_pen_range": 2048,
|
||||
"streaming_kobold": true,
|
||||
"top_p": 0,
|
||||
"top_a": 0,
|
||||
"top_k": 1,
|
||||
"typical": 1,
|
||||
"tfs": 1,
|
||||
"rep_pen_slope": 0.2,
|
||||
"single_line": false,
|
||||
"sampler_order": [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
5
|
||||
]
|
||||
}
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 180,
|
||||
"temp": 0.79,
|
||||
"top_p": 0.9,
|
||||
"top_k": 0,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 1400,
|
||||
"genamt": 180,
|
||||
"temp": 0.65,
|
||||
"top_p": 0.9,
|
||||
"top_k": 0,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.63,
|
||||
"top_k": 0,
|
||||
"top_p": 0.98,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.7,
|
||||
"top_k": 0,
|
||||
"top_p": 0.5,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.7,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 90,
|
||||
"temp": 0.8,
|
||||
"top_p": 0.94,
|
||||
"top_k": 15,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.66,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.94,
|
||||
"top_k": 12,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 1.5,
|
||||
"top_k": 85,
|
||||
"top_p": 0.24,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 1.05,
|
||||
"top_k": 0,
|
||||
"top_p": 0.95,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 1.07,
|
||||
"top_k": 100,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.44,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 1.35,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 1400,
|
||||
"genamt": 80,
|
||||
"temp": 1,
|
||||
"top_p": 1,
|
||||
"top_k": 0,
|
||||
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 100,
|
||||
"genamt": 100,
|
||||
"temp": 1,
|
||||
"top_k": 0,
|
||||
"top_p": 0.95,
|
||||
@@ -19,4 +17,4 @@
|
||||
4,
|
||||
5
|
||||
]
|
||||
}
|
||||
}
|
||||
|
22
public/KoboldAI Settings/Storywriter-Llama2.settings
Normal file
22
public/KoboldAI Settings/Storywriter-Llama2.settings
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"temp": 0.72,
|
||||
"rep_pen": 1.1,
|
||||
"rep_pen_range": 4096,
|
||||
"streaming_kobold": true,
|
||||
"top_p": 0.73,
|
||||
"top_a": 0,
|
||||
"top_k": 0,
|
||||
"typical": 1,
|
||||
"tfs": 1,
|
||||
"rep_pen_slope": 0.2,
|
||||
"single_line": false,
|
||||
"sampler_order": [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
5
|
||||
]
|
||||
}
|
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"max_length": 2048,
|
||||
"genamt": 100,
|
||||
"temp": 0.72,
|
||||
"tfs": 1,
|
||||
"top_a": 0,
|
||||
|
22
public/KoboldAI Settings/simple-proxy-for-tavern.settings
Normal file
22
public/KoboldAI Settings/simple-proxy-for-tavern.settings
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"temp": 0.65,
|
||||
"rep_pen": 1.18,
|
||||
"rep_pen_range": 2048,
|
||||
"streaming_kobold": true,
|
||||
"top_p": 0.47,
|
||||
"top_a": 0,
|
||||
"top_k": 42,
|
||||
"typical": 1,
|
||||
"tfs": 1,
|
||||
"rep_pen_slope": 0,
|
||||
"single_line": false,
|
||||
"sampler_order": [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
5
|
||||
]
|
||||
}
|
19
public/NovelAI Settings/Asper-Kayra.settings
Normal file
19
public/NovelAI Settings/Asper-Kayra.settings
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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",
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "aggressive",
|
||||
"max_context": 7800
|
||||
}
|
19
public/NovelAI Settings/Blended-Coffee-Kayra.settings
Normal file
19
public/NovelAI Settings/Blended-Coffee-Kayra.settings
Normal 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": 7800
|
||||
}
|
20
public/NovelAI Settings/Blook-Kayra.settings
Normal file
20
public/NovelAI Settings/Blook-Kayra.settings
Normal 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": 7800
|
||||
}
|
21
public/NovelAI Settings/Carefree-Kayra.settings
Normal file
21
public/NovelAI Settings/Carefree-Kayra.settings
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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",
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "aggressive",
|
||||
"max_context": 7800
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
0,
|
||||
2
|
||||
],
|
||||
"temperature": 1.05,
|
||||
"max_length": 90,
|
||||
"min_length": 1,
|
||||
"tail_free_sampling": 0.989,
|
||||
"repetition_penalty": 1.5,
|
||||
"repetition_penalty_range": 8192,
|
||||
"repetition_penalty_frequency": 0.03,
|
||||
"repetition_penalty_presence": 0.005,
|
||||
"top_a": 0.075,
|
||||
"top_k": 79,
|
||||
"top_p": 0.95
|
||||
}
|
21
public/NovelAI Settings/Edgewise-Clio.settings
Normal file
21
public/NovelAI Settings/Edgewise-Clio.settings
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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",
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "very_light",
|
||||
"max_context": 7800
|
||||
}
|
@@ -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,
|
||||
@@ -14,5 +16,7 @@
|
||||
"use_cache": false,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"max_context": 8192
|
||||
}
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "very_light",
|
||||
"max_context": 7800
|
||||
}
|
||||
|
20
public/NovelAI Settings/Fresh-Coffee-Kayra.settings
Normal file
20
public/NovelAI Settings/Fresh-Coffee-Kayra.settings
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"order": [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",
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "off",
|
||||
"max_context": 7800
|
||||
}
|
19
public/NovelAI Settings/Green-Active-Writer-Kayra.settings
Normal file
19
public/NovelAI Settings/Green-Active-Writer-Kayra.settings
Normal 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": 7800
|
||||
}
|
@@ -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,
|
||||
@@ -14,5 +16,7 @@
|
||||
"use_cache": false,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"max_context": 8192
|
||||
}
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "very_light",
|
||||
"max_context": 7800
|
||||
}
|
||||
|
@@ -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,
|
||||
@@ -15,5 +16,7 @@
|
||||
"use_cache": false,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"max_context": 8192
|
||||
}
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "very_light",
|
||||
"max_context": 7800
|
||||
}
|
||||
|
22
public/NovelAI Settings/Pilotfish-Kayra.settings
Normal file
22
public/NovelAI Settings/Pilotfish-Kayra.settings
Normal 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": 7800
|
||||
}
|
19
public/NovelAI Settings/Pro_Writer-Kayra.settings
Normal file
19
public/NovelAI Settings/Pro_Writer-Kayra.settings
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"order": [3, 4, 0],
|
||||
"temperature": 1.19,
|
||||
"max_length": 300,
|
||||
"min_length": 1,
|
||||
"top_a": 0.116,
|
||||
"tail_free_sampling": 0.958,
|
||||
"repetition_penalty": 1.64,
|
||||
"repetition_penalty_slope": 2.12,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"repetition_penalty_range": 2048,
|
||||
"use_cache": false,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"phrase_rep_pen": "medium",
|
||||
"cfg_scale": 1.0,
|
||||
"max_context": 7800
|
||||
}
|
17
public/NovelAI Settings/Stelenes-Kayra.settings
Normal file
17
public/NovelAI Settings/Stelenes-Kayra.settings
Normal 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": 7800
|
||||
}
|
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"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",
|
||||
"max_context": 8192
|
||||
}
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "very_light",
|
||||
"max_context": 7800
|
||||
}
|
||||
|
19
public/NovelAI Settings/Tea_Time-Kayra.settings
Normal file
19
public/NovelAI Settings/Tea_Time-Kayra.settings
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"order": [5, 0, 4],
|
||||
"temperature": 1,
|
||||
"max_length": 300,
|
||||
"min_length": 1,
|
||||
"top_a": 0.017,
|
||||
"typical_p": 0.975,
|
||||
"repetition_penalty": 3,
|
||||
"repetition_penalty_slope": 0.09,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"repetition_penalty_range": 7680,
|
||||
"use_cache": false,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"phrase_rep_pen": "aggressive",
|
||||
"cfg_scale": 1.0,
|
||||
"max_context": 7800
|
||||
}
|
18
public/NovelAI Settings/Tesseract-Kayra.settings
Normal file
18
public/NovelAI Settings/Tesseract-Kayra.settings
Normal 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": 7800
|
||||
}
|
@@ -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,
|
||||
@@ -15,5 +16,7 @@
|
||||
"use_cache": false,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"max_context": 8192
|
||||
}
|
||||
"cfg_scale": 1,
|
||||
"phrase_rep_pen": "very_light",
|
||||
"max_context": 7800
|
||||
}
|
||||
|
19
public/NovelAI Settings/Writers-Daemon-Kayra.settings
Normal file
19
public/NovelAI Settings/Writers-Daemon-Kayra.settings
Normal 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": 7800
|
||||
}
|
23
public/QuickReplies/Default.json
Normal file
23
public/QuickReplies/Default.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Default",
|
||||
"quickReplyEnabled": true,
|
||||
"quickReplySlots": [
|
||||
{
|
||||
"mes": "/?",
|
||||
"label": "HELP",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"mes": "/newchat",
|
||||
"label": "New Chat",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"mes": "/bgcol",
|
||||
"label": "Match UI to Background",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"numberOfSlots": 3,
|
||||
"selectedPreset": "Default"
|
||||
}
|
@@ -3,7 +3,7 @@
|
||||
"top_p": 0.9,
|
||||
"top_k": 20,
|
||||
"typical_p": 1,
|
||||
"top_a": 0.75,
|
||||
"top_a": 0,
|
||||
"tfs": 1,
|
||||
"epsilon_cutoff": 0,
|
||||
"eta_cutoff": 0,
|
||||
|
23
public/TextGen Settings/simple-proxy-for-tavern.settings
Normal file
23
public/TextGen Settings/simple-proxy-for-tavern.settings
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"temp": 0.65,
|
||||
"top_p": 0.47,
|
||||
"top_k": 42,
|
||||
"typical_p": 1,
|
||||
"top_a": 0,
|
||||
"tfs": 1,
|
||||
"epsilon_cutoff": 0,
|
||||
"eta_cutoff": 0,
|
||||
"rep_pen": 1.18,
|
||||
"rep_pen_range": 0,
|
||||
"no_repeat_ngram_size": 0,
|
||||
"penalty_alpha": 0,
|
||||
"num_beams": 1,
|
||||
"length_penalty": 1,
|
||||
"min_length": 0,
|
||||
"encoder_rep_pen": 1,
|
||||
"do_sample": true,
|
||||
"early_stopping": false,
|
||||
"mirostat_mode": 0,
|
||||
"mirostat_tau": 5,
|
||||
"mirostat_eta": 0.1
|
||||
}
|
@@ -1523,7 +1523,7 @@
|
||||
"Continue": "이어서 계속",
|
||||
"Editing:": "편집:",
|
||||
"AI reply prefix": "AI 답변 접두사",
|
||||
"Custom Stopping Strings": "문장출력 중단 문자열 (KoboldAI/TextGen)",
|
||||
"Custom Stopping Strings": "문장출력 중단 문자열 (KoboldAI/TextGen/NovelAI)",
|
||||
"JSON serialized array of strings": "JSON 연속 문자배열",
|
||||
"words you dont want generated separated by comma ','": "답변에 포함을 막으려는 단어 (쉼표','로 구분)",
|
||||
"Extensions URL": "확장기능 URL",
|
||||
|
@@ -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>
|
||||
@@ -236,7 +253,7 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="temp" name="volume" min="0.1" max="2.0" step="0.01">
|
||||
<input type="range" id="temp" name="volume" min="0.0" max="2.0" step="0.01">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="temp" id="temp_counter">
|
||||
@@ -311,7 +328,7 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="temp_novel" name="volume" min="0.1" max="2.0" step="0.01">
|
||||
<input type="range" id="temp_novel" name="volume" min="0.1" max="2.50" step="0.01">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="temp_novel" id="temp_counter_novel">
|
||||
@@ -325,7 +342,7 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="rep_pen_novel" name="volume" min="1" max="1.5" step="0.01">
|
||||
<input type="range" id="rep_pen_novel" name="volume" min="1" max="8" step="0.05">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="rep_pen_novel" id="rep_pen_counter_novel">
|
||||
@@ -370,7 +387,7 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="rep_pen_freq_novel" name="volume" min="0" max="1" step="0.00001">
|
||||
<input type="range" id="rep_pen_freq_novel" name="volume" min="0" max="1" step="0.01">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="rep_pen_freq_novel" id="rep_pen_freq_counter_novel">
|
||||
@@ -385,7 +402,7 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="rep_pen_presence_novel" name="volume" min="0" max="1" step="0.001">
|
||||
<input type="range" id="rep_pen_presence_novel" name="volume" min="0" max="1" step="0.01">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="rep_pen_presence_novel" id="rep_pen_presence_counter_novel">
|
||||
@@ -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,127 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="novel_api-settings">
|
||||
<div class="range-block">
|
||||
<div class="range-block-title openai_restorable">
|
||||
<span data-i18n="Preamble">Preamble</span>
|
||||
<div id="nai_preamble_restore" title="Restore default prompt" data-i18n="[title]Restore default prompt"
|
||||
class="right_menu_button">
|
||||
<div class="fa-solid fa-clock-rotate-left "></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Use style tags to modify the writing style of the output">
|
||||
Use style tags to modify the writing style of the output
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<textarea id="nai_preamble_textarea" class="text_pole textarea_compact" name="nai_preamble" rows="2"
|
||||
placeholder=""></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -1184,6 +1335,10 @@
|
||||
<textarea id="jailbreak_prompt_textarea" class="text_pole textarea_compact" name="jailbreak_prompt" rows="6" placeholder=""></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="claude">
|
||||
<span data-i18n="Assistant Prefill">Assistant Prefill</span>
|
||||
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill" rows="3" maxlength="5000" placeholder="Start Claude's answer with..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="inline-drawer wide100p">
|
||||
<div class="inline-drawer-toggle inline-drawer-header margin-bot-10px">
|
||||
@@ -1387,6 +1542,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">
|
||||
@@ -1395,30 +1551,55 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="textgenerationwebui_api" style="display: none;position: relative;">
|
||||
<div class="flex-container">
|
||||
<a href="https://github.com/oobabooga/text-generation-webui" target="_blank">
|
||||
oobabooga/text-generation-webui
|
||||
</a>
|
||||
<span data-i18n="Make sure you run it with">
|
||||
Make sure you run it with <tt>--api</tt> flag
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="Blocking API url">Blocking API url</h4>
|
||||
<small>Example: http://127.0.0.1:5000/</small>
|
||||
<input id="textgenerationwebui_api_url_text" name="textgenerationwebui_api_url" class="text_pole wide100p" maxlength="500" value="" autocomplete="off">
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="Streaming API url">Streaming API url</h4>
|
||||
<small>Example: ws://127.0.0.1:5005/api/v1/stream</small>
|
||||
<input id="streaming_url_textgenerationwebui" type="text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off">
|
||||
<form action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
If you are using:
|
||||
<div class="flex-container indent20p">
|
||||
<a href="https://github.com/oobabooga/text-generation-webui" target="_blank">
|
||||
oobabooga/text-generation-webui
|
||||
</a>
|
||||
<span data-i18n="Make sure you run it with">
|
||||
Make sure you run it with <tt>--api</tt> flag
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-container indent20p">
|
||||
<a href="https://mancer.tech/" target="_blank">
|
||||
Mancer AI
|
||||
</a>
|
||||
<label class="checkbox_label" for="use-mancer-api-checkbox">
|
||||
<span data-i18n="Use API key (Only required for Mancer)">
|
||||
Click this box (and add your API key!):
|
||||
</span>
|
||||
<input id="use-mancer-api-checkbox" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="mancer-api-ui" style="display:none;">
|
||||
<h4 data-i18n="Mancer API key">Mancer API key</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_mancer" name="api_key_mancer" class="text_pole flex1 wide100p" maxlength="500" size="35" 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_mancer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input id="api_button_textgenerationwebui" class="menu_button" type="submit" value="Connect">
|
||||
<div id="api_loading_textgenerationwebui" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div data-for="api_key_mancer" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="Blocking API url">Blocking API url</h4>
|
||||
<small>Example: http://127.0.0.1:5000/</small>
|
||||
<input id="textgenerationwebui_api_url_text" name="textgenerationwebui_api_url" class="text_pole wide100p" maxlength="500" value="" autocomplete="off">
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="Streaming API url">Streaming API url</h4>
|
||||
<small>Example: ws://127.0.0.1:5005/api/v1/stream</small>
|
||||
<input id="streaming_url_textgenerationwebui" type="text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<input id="api_button_textgenerationwebui" class="menu_button" type="submit" value="Connect">
|
||||
<div id="api_loading_textgenerationwebui" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="online_status4">
|
||||
<div class="online_status_indicator4"></div>
|
||||
<div class="online_status_text4" data-i18n="Not connected">Not connected</div>
|
||||
@@ -1458,6 +1639,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>
|
||||
@@ -1660,7 +1846,7 @@
|
||||
<span data-i18n="Trim Incomplete Sentences">Trim Incomplete Sentences</span>
|
||||
</label>
|
||||
<!-- Add margin since this is a child of above -->
|
||||
<label style="margin-left: 1em;" class="checkbox_label" for="include_newline_checkbox">
|
||||
<label class="checkbox_label indent20p" for="include_newline_checkbox">
|
||||
<input id="include_newline_checkbox" type="checkbox" />
|
||||
<span data-i18n="Include Newline">Include Newline</span>
|
||||
</label>
|
||||
@@ -1669,7 +1855,7 @@
|
||||
Custom Chat Separator
|
||||
</h4>
|
||||
<div>
|
||||
<input id="custom_chat_separator" class="text_pole textarea_compact" type="text" placeholder="<START>" maxlength="100" />
|
||||
<textarea id="custom_chat_separator" class="text_pole textarea_compact" type="text" placeholder="<START>" maxlength="500" rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1704,6 +1890,10 @@
|
||||
<input id="instruct_names" type="checkbox" />
|
||||
<span data-i18n="Include Names">Include Names</span>
|
||||
</label>
|
||||
<label for="instruct_names_force_groups" class="checkbox_label indent20p">
|
||||
<input id="instruct_names_force_groups" type="checkbox" />
|
||||
<span data-i18n="Force for Groups and Personas">Force for Groups and Personas</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for="instruct_presets">
|
||||
<span data-i18n="Presets">Presets</span>
|
||||
@@ -1722,7 +1912,7 @@
|
||||
<span data-i18n="Input Sequence">Input Sequence</span>
|
||||
</label>
|
||||
<div>
|
||||
<input id="instruct_input_sequence" class="text_pole textarea_compact" type="text" maxlength="100" />
|
||||
<textarea id="instruct_input_sequence" class="text_pole textarea_compact" type="text" maxlength="500" rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
@@ -1730,7 +1920,7 @@
|
||||
<span data-i18n="Output Sequence">Output Sequence</span>
|
||||
</label>
|
||||
<div>
|
||||
<input id="instruct_output_sequence" class="text_pole wide100p textarea_compact" type="text" maxlength="100" />
|
||||
<textarea id="instruct_output_sequence" class="text_pole wide100p textarea_compact" type="text" maxlength="500" rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1740,7 +1930,7 @@
|
||||
<small data-i18n="System Sequence">System Sequence</small>
|
||||
</label>
|
||||
<div>
|
||||
<input id="instruct_system_sequence" class="text_pole textarea_compact" type="text" maxlength="100" />
|
||||
<textarea id="instruct_system_sequence" class="text_pole textarea_compact" type="text" maxlength="500" rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
@@ -1748,7 +1938,7 @@
|
||||
<small data-i18n="Stop Sequence">Stop Sequence</small>
|
||||
</label>
|
||||
<div>
|
||||
<input id="instruct_stop_sequence" class="text_pole wide100p textarea_compact" type="text" maxlength="100" />
|
||||
<textarea id="instruct_stop_sequence" class="text_pole wide100p textarea_compact" type="text" maxlength="500" rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
@@ -1756,7 +1946,7 @@
|
||||
<small data-i18n="Separator">Separator</small>
|
||||
</label>
|
||||
<div>
|
||||
<input id="instruct_separator_sequence" class="text_pole wide100p textarea_compact" type="text" maxlength="100" />
|
||||
<textarea id="instruct_separator_sequence" class="text_pole wide100p textarea_compact" type="text" maxlength="500" rows="1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1771,12 +1961,13 @@
|
||||
</a>
|
||||
</h4>
|
||||
<select id="tokenizer">
|
||||
<option value="99">Best match (recommended)</option>
|
||||
<option value="0">None / Estimated</option>
|
||||
<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>
|
||||
@@ -1835,7 +2026,7 @@
|
||||
</label>
|
||||
<h4>
|
||||
<span data-i18n="Custom Stopping Strings">
|
||||
Custom Stopping Strings (KoboldAI/TextGen)
|
||||
Custom Stopping Strings (KoboldAI/TextGen/NovelAI)
|
||||
</span>
|
||||
<div>
|
||||
<small>
|
||||
@@ -1847,6 +2038,12 @@
|
||||
<div>
|
||||
<textarea id="custom_stopping_strings" rows="2" class="text_pole textarea_compact" placeholder="["Ford", "BMW", "Fiat"]"></textarea>
|
||||
</div>
|
||||
<label class="checkbox_label" for="custom_stopping_strings_macro">
|
||||
<input id="custom_stopping_strings_macro" type="checkbox" checked>
|
||||
<span data-i18n="Replace Macro in Custom Stopping Strings">
|
||||
Replace Macro in Custom Stopping Strings
|
||||
</span>
|
||||
</label>
|
||||
<h4>
|
||||
<span data-i18n="Pygmalion Formatting">
|
||||
Pygmalion Formatting
|
||||
@@ -1996,6 +2193,12 @@
|
||||
Match whole words
|
||||
</small>
|
||||
</label>
|
||||
<label title="Alert if your world info is greater than the allocated budget." data-i18n="[title]Alert if your world info is greater than the allocated budget." class="checkbox_label">
|
||||
<input id="world_info_overflow_alert" type="checkbox" />
|
||||
<small data-i18n="Alert On Overflow">
|
||||
Alert On Overflow
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2280,6 +2483,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 +2547,10 @@
|
||||
<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>
|
||||
<label for="relaxed_api_urls" title="Reduce the formatting requirements on API URLS"><input id="relaxed_api_urls" type="checkbox" />
|
||||
<span data-i18n="Relaxed API URLS">Relaxed API URLS</span>
|
||||
</label>
|
||||
|
||||
<div class="inline-drawer wide100p flexFlowColumn">
|
||||
@@ -2464,16 +2674,24 @@
|
||||
<option value="3" data-i18n="Bottom of Author's Note">Bottom of Author's Note</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<label for="persona_show_notifications" class="checkbox_label">
|
||||
<input id="persona_show_notifications" type="checkbox" />
|
||||
<span data-i18n="Show Notifications Show notifications on switching personas">
|
||||
Show notifications on switching personas
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="Your Avatar" class="title_restorable">
|
||||
<span>Your Persona</span>
|
||||
<h4 class="title_restorable">
|
||||
<span data-i18n="Your Persona">Your Persona</span>
|
||||
<button class="menu_button menu_button_icon user_stats_button" title="Click for stats!">
|
||||
<i class="fa-solid fa-circle-info"></i>Usage Stats
|
||||
<i class="fa-solid fa-circle-info"></i><span data-i18n="Usage Stats">Usage Stats</span>
|
||||
</button>
|
||||
<div id="create_dummy_persona" class="menu_button menu_button_icon" title="Create a dummy persona" data-i18n="[title]Create a dummy persona">
|
||||
<i class="fa-solid fa-person-circle-question fa-fw"></i>
|
||||
<span>Blank</span>
|
||||
<span data-i18n="Blank">Blank</span>
|
||||
</div>
|
||||
</h4>
|
||||
<div id="user_avatar_block">
|
||||
@@ -2780,6 +2998,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>
|
||||
@@ -3099,7 +3319,7 @@
|
||||
<div title="Rename chat file" class="renameChatButton fa-solid fa-pen" data-i18n="[title]Rename chat file"></div>
|
||||
<div title="Export JSONL chat file" data-format="jsonl" class="exportRawChatButton fa-solid fa-file-export" data-i18n="[title]Export JSONL chat file"></div>
|
||||
<div title="Download chat as plain text document" data-format="txt" class="exportChatButton fa-solid fa-file-lines" data-i18n="[title]Download chat as plain text document"></div>
|
||||
<div title="Delete chat file" file_name="" class="PastChat_cross fa-solid fa-circle-xmark" data-i18n="[title]Delete chat file"></div>
|
||||
<div title="Delete chat file" file_name="" class="PastChat_cross fa-solid fa-skull" data-i18n="[title]Delete chat file"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3300,7 +3520,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 />
|
||||
@@ -3460,7 +3681,7 @@
|
||||
</div>
|
||||
|
||||
<div id="hotswap_template" class="template_element">
|
||||
<div class="hotswapAvatar">
|
||||
<div class="hotswapAvatar" title="Add a character/group to favorites to display it here!">
|
||||
<img src="/img/ai4.png">
|
||||
</div>
|
||||
</div>
|
||||
@@ -3618,6 +3839,14 @@
|
||||
<!-- popups live outside sheld to avoid blur conflicts -->
|
||||
<div id="options" class="font-family-reset" style="display: none;">
|
||||
<div class="options-content">
|
||||
<a id="option_close_chat" class="displayNone">
|
||||
<i class="fa-lg fa-solid fa-times"></i>
|
||||
<span data-i18n="Close chat">Close chat</span>
|
||||
</a>
|
||||
<a id="option_settings" class="displayNone">
|
||||
<i class="fa-lg fa-solid fa-cog"></i>
|
||||
<span data-i18n="Toggle Panels">Toggle Panels</span>
|
||||
</a>
|
||||
<a id="option_toggle_AN">
|
||||
<i class="fa-lg fa-solid fa-note-sticky"></i>
|
||||
<span data-i18n="Author's Note">Author's Note</span>
|
||||
|
@@ -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]",
|
||||
|
12
public/instruct/OpenOrca-OpenChat.json
Normal file
12
public/instruct/OpenOrca-OpenChat.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"input_sequence": "User: ",
|
||||
"macro": true,
|
||||
"name": "OpenOrca/OpenChat",
|
||||
"names": true,
|
||||
"output_sequence": "<|end_of_turn|>\nAssistant: ",
|
||||
"separator_sequence": "<|end_of_turn|>\n",
|
||||
"stop_sequence": "",
|
||||
"system_prompt": "You are a helpful assistant. Please answer truthfully and write out your thinking step by step to be sure you get the right answer. If you make a mistake or encounter an error in your thinking, say so out loud and attempt to correct it. If you don't know or aren't sure about something, say so clearly. You will act as a professional logician, mathematician, and physicist. You will also act as the most appropriate type of expert to answer any particular question or solve the relevant problem; state which expert type your are, if so. Also think of any particular named expert that would be ideal to answer the relevant question or solve the relevant problem; name and act as them, if appropriate.\n",
|
||||
"system_sequence": "",
|
||||
"wrap": false
|
||||
}
|
12
public/instruct/Roleplay.json
Normal file
12
public/instruct/Roleplay.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"input_sequence": "### Instruction:",
|
||||
"macro": true,
|
||||
"name": "Roleplay",
|
||||
"names": false,
|
||||
"output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):",
|
||||
"separator_sequence": "",
|
||||
"stop_sequence": "",
|
||||
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\nAvoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.\n\n### Input:",
|
||||
"system_sequence": "",
|
||||
"wrap": true
|
||||
}
|
629
public/script.js
629
public/script.js
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,10 @@ import {
|
||||
menu_type,
|
||||
max_context,
|
||||
saveSettingsDebounced,
|
||||
active_group,
|
||||
active_character,
|
||||
setActiveGroup,
|
||||
setActiveCharacter,
|
||||
} from "../script.js";
|
||||
|
||||
import {
|
||||
@@ -239,7 +243,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() {
|
||||
@@ -331,11 +334,21 @@ export function RA_CountCharTokens() {
|
||||
characterStatsHandler(characters, this_chid);
|
||||
});
|
||||
}
|
||||
//Auto Load Last Charcter -- (fires when active_character is defined and auto_load_chat is true)
|
||||
/**
|
||||
* Auto load chat with the last active character or group.
|
||||
* Fires when active_character is defined and auto_load_chat is true.
|
||||
* The function first tries to find a character with a specific ID from the global settings.
|
||||
* If it doesn't exist, it tries to find a group with a specific grid from the global settings.
|
||||
* If the character list hadn't been loaded yet, it calls itself again after 100ms delay.
|
||||
* The character or group is selected (clicked) if it is found.
|
||||
*/
|
||||
async function RA_autoloadchat() {
|
||||
if (document.getElementById('CharID0') !== null) {
|
||||
var charToAutoLoad = document.getElementById('CharID' + LoadLocal('ActiveChar'));
|
||||
let groupToAutoLoad = document.querySelector(`.group_select[grid="${LoadLocal('ActiveGroup')}"]`);
|
||||
// active character is the name, we should look it up in the character list and get the id
|
||||
let active_character_id = Object.keys(characters).find(key => characters[key].avatar === active_character);
|
||||
|
||||
var charToAutoLoad = document.getElementById('CharID' + active_character_id);
|
||||
let groupToAutoLoad = document.querySelector(`.group_select[grid="${active_group}"]`);
|
||||
if (charToAutoLoad != null) {
|
||||
$(charToAutoLoad).click();
|
||||
}
|
||||
@@ -343,7 +356,7 @@ async function RA_autoloadchat() {
|
||||
$(groupToAutoLoad).click();
|
||||
}
|
||||
|
||||
// if the charcter list hadn't been loaded yet, try again.
|
||||
// if the character list hadn't been loaded yet, try again.
|
||||
} else { setTimeout(RA_autoloadchat, 100); }
|
||||
}
|
||||
|
||||
@@ -367,6 +380,7 @@ export async function favsToHotswap() {
|
||||
thisHotSwapSlot.attr('grid', isGroup ? grid : '');
|
||||
thisHotSwapSlot.attr('chid', isCharacter ? chid : '');
|
||||
thisHotSwapSlot.data('id', isGroup ? grid : chid);
|
||||
thisHotSwapSlot.attr('title', '');
|
||||
|
||||
if (isGroup) {
|
||||
const group = groups.find(x => x.id === grid);
|
||||
@@ -448,8 +462,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)
|
||||
@@ -521,12 +535,14 @@ export function dragElement(elmnt) {
|
||||
|
||||
if (elmntHeader.length) {
|
||||
elmntHeader.off('mousedown').on('mousedown', (e) => {
|
||||
|
||||
hasBeenDraggedByUser = true
|
||||
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
|
||||
dragMouseDown(e);
|
||||
});
|
||||
$(elmnt).off('mousedown').on('mousedown', () => { isMouseDown = true })
|
||||
} else {
|
||||
elmnt.off('mousedown').on('mousedown', dragMouseDown);
|
||||
$(elmnt).off('mousedown').on('mousedown', () => {
|
||||
isMouseDown = true
|
||||
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
|
||||
})
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
@@ -605,7 +621,7 @@ export function dragElement(elmnt) {
|
||||
}
|
||||
|
||||
//prevent resizing from top left into the top bar
|
||||
if (top <= 40 && maxX >= topBarFirstX && left <= topBarFirstX
|
||||
if (top < 40 && maxX >= topBarFirstX && left <= topBarFirstX
|
||||
) {
|
||||
console.debug('prevent topbar underlap resize')
|
||||
elmnt.css('width', width - 1 + "px");
|
||||
@@ -661,8 +677,6 @@ export function dragElement(elmnt) {
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
|
||||
|
||||
function dragMouseDown(e) {
|
||||
|
||||
if (e) {
|
||||
@@ -732,6 +746,7 @@ export function dragElement(elmnt) {
|
||||
$("body").css("overflow", "");
|
||||
// Clear the "data-dragged" attribute
|
||||
elmnt.attr('data-dragged', 'false');
|
||||
observer.disconnect()
|
||||
console.debug(`Saving ${elmntName} UI position`)
|
||||
saveSettingsDebounced();
|
||||
|
||||
@@ -904,16 +919,22 @@ $("document").ready(function () {
|
||||
$("#rm_button_characters").click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });
|
||||
|
||||
// when a char is selected from the list, save them as the auto-load character for next page load
|
||||
|
||||
// when a char is selected from the list, save their name as the auto-load character for next page load
|
||||
$(document).on("click", ".character_select", function () {
|
||||
SaveLocal('ActiveChar', $(this).attr('chid'));
|
||||
SaveLocal('ActiveGroup', null);
|
||||
setActiveCharacter($(this).find('.avatar').attr('title'));
|
||||
setActiveGroup(null);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on("click", ".group_select", function () {
|
||||
SaveLocal('ActiveChar', null);
|
||||
SaveLocal('ActiveGroup', $(this).data('id'));
|
||||
setActiveCharacter(null);
|
||||
setActiveGroup($(this).data('id'));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
|
||||
|
||||
//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
|
||||
$('#send_textarea').on('input', function () {
|
||||
this.style.height = '40px';
|
||||
@@ -968,6 +989,12 @@ $("document").ready(function () {
|
||||
Generate();
|
||||
}
|
||||
}
|
||||
if ($(':focus').attr('id') === 'dialogue_popup_input' && !isMobile()) {
|
||||
if (!event.shiftKey && !event.ctrlKey && event.key == "Enter") {
|
||||
event.preventDefault();
|
||||
$('#dialogue_popup_ok').trigger('click');
|
||||
}
|
||||
}
|
||||
//ctrl+shift+up to scroll to context line
|
||||
if (event.shiftKey && event.ctrlKey && event.key == "ArrowUp") {
|
||||
event.preventDefault();
|
||||
@@ -1029,11 +1056,10 @@ $("document").ready(function () {
|
||||
}
|
||||
|
||||
if (event.ctrlKey && event.key == "ArrowUp") { //edits last USER message if chatbar is empty and focused
|
||||
console.debug('got ctrl+uparrow input');
|
||||
if (
|
||||
$("#send_textarea").val() === '' &&
|
||||
chatbarInFocus === true &&
|
||||
$(".swipe_right:last").css('display') === 'flex' &&
|
||||
($(".swipe_right:last").css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') &&
|
||||
$("#character_popup").css("display") === "none" &&
|
||||
$("#shadow_select_chat_popup").css("display") === "none"
|
||||
) {
|
||||
@@ -1041,7 +1067,7 @@ $("document").ready(function () {
|
||||
const lastIsUserMes = isUserMesList[isUserMesList.length - 1];
|
||||
const editMes = lastIsUserMes.querySelector('.mes_block .mes_edit');
|
||||
if (editMes !== null) {
|
||||
$(editMes).click();
|
||||
$(editMes).trigger('click');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1062,5 +1088,65 @@ $("document").ready(function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key == "Escape") { //closes various panels
|
||||
if ($("#curEditTextarea").is(":visible")) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($("#dialogue_popup").is(":visible")) {
|
||||
if ($("#dialogue_popup_cancel").is(":visible")) {
|
||||
$("#dialogue_popup_cancel").trigger('click');
|
||||
return
|
||||
} else {
|
||||
$("#dialogue_popup_ok").trigger('click')
|
||||
return
|
||||
}
|
||||
}
|
||||
if ($("#select_chat_popup").is(":visible")) {
|
||||
$("#select_chat_cross").trigger('click');
|
||||
return
|
||||
}
|
||||
if ($("#character_popup").is(":visible")) {
|
||||
$("#character_cross").trigger('click');
|
||||
return
|
||||
}
|
||||
|
||||
if ($(".drawer-content")
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
.not('#right-nav-panel')
|
||||
.is(":visible")) {
|
||||
let visibleDrawerContent = $(".drawer-content:visible")
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
.not('#right-nav-panel')
|
||||
$(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
|
||||
return
|
||||
}
|
||||
|
||||
if ($("#floatingPrompt").is(":visible")) {
|
||||
$("#ANClose").trigger('click');
|
||||
return
|
||||
}
|
||||
if ($("#WorldInfo").is(":visible")) {
|
||||
$("#WIDrawerIcon").trigger('click');
|
||||
return
|
||||
}
|
||||
if ($("#left-nav-panel").is(":visible")) {
|
||||
$("#leftNavDrawerIcon").trigger('click');
|
||||
return
|
||||
}
|
||||
if ($("#right-nav-panel").is(":visible")) {
|
||||
$("#rightNavDrawerIcon").trigger('click');
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (event.ctrlKey && /^[1-9]$/.test(event.key)) {
|
||||
// Your code here
|
||||
event.preventDefault();
|
||||
console.log("Ctrl +" + event.key + " pressed!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -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 = [];
|
||||
|
117
public/scripts/extensions/bulk-edit/index.js
Normal file
117
public/scripts/extensions/bulk-edit/index.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../../../script.js";
|
||||
|
||||
let is_bulk_edit = false;
|
||||
|
||||
/**
|
||||
* Toggles bulk edit mode on/off when the edit button is clicked.
|
||||
*/
|
||||
function onEditButtonClick() {
|
||||
console.log("Edit button clicked");
|
||||
// toggle bulk edit mode
|
||||
if (is_bulk_edit) {
|
||||
disableBulkSelect();
|
||||
// hide the delete button
|
||||
$("#bulkDeleteButton").hide();
|
||||
is_bulk_edit = false;
|
||||
} else {
|
||||
enableBulkSelect();
|
||||
// show the delete button
|
||||
$("#bulkDeleteButton").show();
|
||||
is_bulk_edit = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the character with the given chid.
|
||||
*
|
||||
* @param {string} this_chid - The chid of the character to delete.
|
||||
*/
|
||||
async function deleteCharacter(this_chid) {
|
||||
await handleDeleteCharacter("del_ch", this_chid, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all characters that have been selected via the bulk checkboxes.
|
||||
*/
|
||||
async function onDeleteButtonClick() {
|
||||
console.log("Delete button clicked");
|
||||
|
||||
// Create a mapping of chid to avatar
|
||||
let toDelete = [];
|
||||
$(".bulk_select_checkbox:checked").each((i, el) => {
|
||||
const chid = $(el).parent().attr("chid");
|
||||
const avatar = characters[chid].avatar;
|
||||
// Add the avatar to the list of avatars to delete
|
||||
toDelete.push(avatar);
|
||||
});
|
||||
|
||||
const confirm = await callPopup('<h3>Are you sure you want to delete these characters?</h3>You would need to delete the chat files manually.<br>', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
console.log('User cancelled delete');
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the characters
|
||||
for (const avatar of toDelete) {
|
||||
console.log(`Deleting character with avatar ${avatar}`);
|
||||
await getCharacters();
|
||||
|
||||
//chid should be the key of the character with the given avatar
|
||||
const chid = Object.keys(characters).find((key) => characters[key].avatar === avatar);
|
||||
console.log(`Deleting character with chid ${chid}`);
|
||||
await deleteCharacter(chid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the bulk edit and delete buttons to the UI.
|
||||
*/
|
||||
function addButtons() {
|
||||
const editButton = $(
|
||||
"<i id='bulkEditButton' class='fa-solid fa-edit menu_button bulkEditButton' title='Bulk edit characters'></i>"
|
||||
);
|
||||
const deleteButton = $(
|
||||
"<i id='bulkDeleteButton' class='fa-solid fa-trash menu_button bulkDeleteButton' title='Bulk delete characters' style='display: none;'></i>"
|
||||
);
|
||||
|
||||
$("#charListGridToggle").after(editButton, deleteButton);
|
||||
|
||||
$("#bulkEditButton").on("click", onEditButtonClick);
|
||||
$("#bulkDeleteButton").on("click", onDeleteButtonClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables bulk selection by adding a checkbox next to each character.
|
||||
*/
|
||||
function enableBulkSelect() {
|
||||
$("#rm_print_characters_block .character_select").each((i, el) => {
|
||||
const character = $(el).text();
|
||||
const checkbox = $("<input type='checkbox' class='bulk_select_checkbox'>");
|
||||
checkbox.on("change", () => {
|
||||
// Do something when the checkbox is changed
|
||||
});
|
||||
$(el).prepend(checkbox);
|
||||
});
|
||||
$("#rm_print_characters_block").addClass("bulk_select");
|
||||
// We also need to disable the default click event for the character_select divs
|
||||
$(document).on("click", ".bulk_select_checkbox", function (event) {
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables bulk selection by removing the checkboxes.
|
||||
*/
|
||||
function disableBulkSelect() {
|
||||
$(".bulk_select_checkbox").remove();
|
||||
$("#rm_print_characters_block").removeClass("bulk_select");
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point that runs on page load.
|
||||
*/
|
||||
jQuery(async () => {
|
||||
addButtons();
|
||||
// loadSettings();
|
||||
});
|
11
public/scripts/extensions/bulk-edit/manifest.json
Normal file
11
public/scripts/extensions/bulk-edit/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Bulk Card Editor",
|
||||
"loading_order": 9,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "city-unit",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/city-unit"
|
||||
}
|
7
public/scripts/extensions/bulk-edit/style.css
Normal file
7
public/scripts/extensions/bulk-edit/style.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.bulk_select_checkbox {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#rm_print_characters_block.bulk_select .wide100pLess70px {
|
||||
width: calc(100% - 85px);
|
||||
}
|
@@ -9,6 +9,7 @@ const MODULE_NAME = 'expressions';
|
||||
const UPDATE_INTERVAL = 2000;
|
||||
const FALLBACK_EXPRESSION = 'joy';
|
||||
const DEFAULT_EXPRESSIONS = [
|
||||
"live2d",
|
||||
"admiration",
|
||||
"amusement",
|
||||
"anger",
|
||||
@@ -392,6 +393,108 @@ function onExpressionsShowDefaultInput() {
|
||||
}
|
||||
}
|
||||
|
||||
async function unloadLiveChar() {
|
||||
try {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/live2d/unload';
|
||||
const loadResponse = await doExtrasFetch(url);
|
||||
if (!loadResponse.ok) {
|
||||
throw new Error(loadResponse.statusText);
|
||||
}
|
||||
const loadResponseText = await loadResponse.text();
|
||||
//console.log(`Response: ${loadResponseText}`);
|
||||
} catch (error) {
|
||||
//console.error(`Error unloading - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLiveChar() {
|
||||
if (!modules.includes('live2d')) {
|
||||
console.debug('live2d module is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
let spriteFolderName = context.name2;
|
||||
const message = getLastCharacterMessage();
|
||||
const avatarFileName = getSpriteFolderName(message);
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
spriteFolderName = expressionOverride.path;
|
||||
}
|
||||
|
||||
const live2dPath = `/characters/${encodeURIComponent(spriteFolderName)}/live2d.png`;
|
||||
|
||||
try {
|
||||
const spriteResponse = await fetch(live2dPath);
|
||||
|
||||
if (!spriteResponse.ok) {
|
||||
throw new Error(spriteResponse.statusText);
|
||||
}
|
||||
|
||||
const spriteBlob = await spriteResponse.blob();
|
||||
const spriteFile = new File([spriteBlob], 'live2d.png', { type: 'image/png' });
|
||||
const formData = new FormData();
|
||||
formData.append('file', spriteFile);
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/live2d/load';
|
||||
|
||||
const loadResponse = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!loadResponse.ok) {
|
||||
throw new Error(loadResponse.statusText);
|
||||
}
|
||||
|
||||
const loadResponseText = await loadResponse.text();
|
||||
console.log(`Load live2d response: ${loadResponseText}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error loading live2d image: ${live2dPath} - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageChange() {
|
||||
const imgElement = document.querySelector('img#expression-image.expression');
|
||||
|
||||
if (!imgElement) {
|
||||
console.log("Cannot find addExpressionImage()");
|
||||
return;
|
||||
}
|
||||
|
||||
if (extension_settings.expressions.live2d) {
|
||||
// Method get IP of endpoint
|
||||
const live2dResultFeedSrc = `${getApiUrl()}/api/live2d/result_feed`;
|
||||
$('#expression-holder').css({ display: '' });
|
||||
if (imgElement.src !== live2dResultFeedSrc) {
|
||||
const expressionImageElement = document.querySelector('.expression_list_image');
|
||||
|
||||
if (expressionImageElement) {
|
||||
doExtrasFetch(expressionImageElement.src, {
|
||||
method: 'HEAD',
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
imgElement.src = live2dResultFeedSrc;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error); // Log the error if necessary
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imgElement.src = ""; //remove incase char doesnt have expressions
|
||||
setExpression(getContext().name2, FALLBACK_EXPRESSION, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
const context = getContext();
|
||||
|
||||
@@ -405,6 +508,16 @@ async function moduleWorker() {
|
||||
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
|
||||
removeExpression();
|
||||
spriteCache = {};
|
||||
|
||||
//clear expression
|
||||
let imgElement = document.getElementById('expression-image');
|
||||
imgElement.src = "";
|
||||
|
||||
//set checkbox to global var
|
||||
$('#image_type_toggle').prop('checked', extension_settings.expressions.live2d);
|
||||
if(extension_settings.expressions.live2d == true){
|
||||
setLive2dState(extension_settings.expressions.live2d);
|
||||
}
|
||||
}
|
||||
|
||||
const vnMode = isVisualNovelMode();
|
||||
@@ -507,6 +620,64 @@ async function moduleWorker() {
|
||||
lastCharacter = context.groupId || context.characterId;
|
||||
lastMessage = currentLastMessage.mes;
|
||||
}
|
||||
}
|
||||
|
||||
async function live2dcheck() {
|
||||
const context = getContext();
|
||||
let spriteFolderName = context.name2;
|
||||
const message = getLastCharacterMessage();
|
||||
const avatarFileName = getSpriteFolderName(message);
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
spriteFolderName = expressionOverride.path;
|
||||
}
|
||||
|
||||
try {
|
||||
await validateImages(spriteFolderName);
|
||||
|
||||
let live2dObj = spriteCache[spriteFolderName].find(obj => obj.label === 'live2d');
|
||||
let live2dPath_f = live2dObj ? live2dObj.path : null;
|
||||
|
||||
if(live2dPath_f != null){
|
||||
//console.log("live2dPath_f " + live2dPath_f);
|
||||
return true;
|
||||
} else {
|
||||
//console.log("live2dPath_f is null");
|
||||
unloadLiveChar();
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
function setLive2dState(switch_var){
|
||||
extension_settings.expressions.live2d = switch_var; // Store setting
|
||||
saveSettingsDebounced();
|
||||
|
||||
live2dcheck().then(result => {
|
||||
if (result) {
|
||||
//console.log("Live2d exists!");
|
||||
|
||||
if (extension_settings.expressions.live2d) {
|
||||
loadLiveChar();
|
||||
} else {
|
||||
unloadLiveChar();
|
||||
}
|
||||
handleImageChange(switch_var); // Change image as needed
|
||||
|
||||
|
||||
} else {
|
||||
//console.log("Live2d does not exist.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function getSpriteFolderName(message) {
|
||||
@@ -654,7 +825,6 @@ async function getSpritesList(name) {
|
||||
|
||||
try {
|
||||
const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`);
|
||||
|
||||
let sprites = result.ok ? (await result.json()) : [];
|
||||
return sprites;
|
||||
}
|
||||
@@ -697,114 +867,137 @@ async function getExpressionsList() {
|
||||
}
|
||||
|
||||
async function setExpression(character, expression, force) {
|
||||
console.debug('entered setExpressions');
|
||||
await validateImages(character);
|
||||
const img = $('img.expression');
|
||||
const prevExpressionSrc = img.attr('src');
|
||||
const expressionClone = img.clone()
|
||||
if (extension_settings.expressions.live2d == false) {
|
||||
|
||||
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
||||
console.debug('checking for expression images to show..');
|
||||
if (sprite) {
|
||||
console.debug('setting expression from character images folder');
|
||||
console.debug('entered setExpressions');
|
||||
await validateImages(character);
|
||||
const img = $('img.expression');
|
||||
const prevExpressionSrc = img.attr('src');
|
||||
const expressionClone = img.clone()
|
||||
|
||||
if (force && isVisualNovelMode()) {
|
||||
const context = getContext();
|
||||
const group = context.groups.find(x => x.id === context.groupId);
|
||||
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
||||
console.debug('checking for expression images to show..');
|
||||
if (sprite) {
|
||||
console.debug('setting expression from character images folder');
|
||||
|
||||
for (const member of group.members) {
|
||||
const groupMember = context.characters.find(x => x.avatar === member);
|
||||
if (force && isVisualNovelMode()) {
|
||||
const context = getContext();
|
||||
const group = context.groups.find(x => x.id === context.groupId);
|
||||
|
||||
if (!groupMember) {
|
||||
continue;
|
||||
for (const member of group.members) {
|
||||
const groupMember = context.characters.find(x => x.avatar === member);
|
||||
|
||||
if (!groupMember) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (groupMember.name == character) {
|
||||
await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
//only swap expressions when necessary
|
||||
if (prevExpressionSrc !== sprite.path
|
||||
&& !img.hasClass('expression-animating')) {
|
||||
//clone expression
|
||||
expressionClone.addClass('expression-clone')
|
||||
//make invisible and remove id to prevent double ids
|
||||
//must be made invisible to start because they share the same Z-index
|
||||
expressionClone.attr('id', '').css({ opacity: 0 });
|
||||
//add new sprite path to clone src
|
||||
expressionClone.attr('src', sprite.path);
|
||||
//add invisible clone to html
|
||||
expressionClone.appendTo($("#expression-holder"))
|
||||
|
||||
if (groupMember.name == character) {
|
||||
await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
|
||||
return;
|
||||
const duration = 200;
|
||||
|
||||
//add animation flags to both images
|
||||
//to prevent multiple expression changes happening simultaneously
|
||||
img.addClass('expression-animating');
|
||||
|
||||
// Set the parent container's min width and height before running the transition
|
||||
const imgWidth = img.width();
|
||||
const imgHeight = img.height();
|
||||
const expressionHolder = img.parent();
|
||||
expressionHolder.css('min-width', imgWidth > 100 ? imgWidth : 100);
|
||||
expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
|
||||
|
||||
//position absolute prevent the original from jumping around during transition
|
||||
img.css('position', 'absolute');
|
||||
expressionClone.addClass('expression-animating');
|
||||
//fade the clone in
|
||||
expressionClone.css({
|
||||
opacity: 0
|
||||
}).animate({
|
||||
opacity: 1
|
||||
}, duration)
|
||||
//when finshed fading in clone, fade out the original
|
||||
.promise().done(function () {
|
||||
img.animate({
|
||||
opacity: 0
|
||||
}, duration);
|
||||
//remove old expression
|
||||
img.remove();
|
||||
//replace ID so it becomes the new 'original' expression for next change
|
||||
expressionClone.attr('id', 'expression-image');
|
||||
expressionClone.removeClass('expression-animating');
|
||||
|
||||
// Reset the expression holder min height and width
|
||||
expressionHolder.css('min-width', 100);
|
||||
expressionHolder.css('min-height', 100);
|
||||
});
|
||||
|
||||
|
||||
expressionClone.removeClass('expression-clone');
|
||||
|
||||
expressionClone.removeClass('default');
|
||||
expressionClone.off('error');
|
||||
expressionClone.on('error', function () {
|
||||
console.debug('Expression image error', sprite.path);
|
||||
$(this).attr('src', '');
|
||||
$(this).off('error');
|
||||
if (force && extension_settings.expressions.showDefault) {
|
||||
setDefault();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (extension_settings.expressions.showDefault) {
|
||||
setDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
//only swap expressions when necessary
|
||||
if (prevExpressionSrc !== sprite.path
|
||||
&& !img.hasClass('expression-animating')) {
|
||||
//clone expression
|
||||
expressionClone.addClass('expression-clone')
|
||||
//make invisible and remove id to prevent double ids
|
||||
//must be made invisible to start because they share the same Z-index
|
||||
expressionClone.attr('id', '').css({ opacity: 0 });
|
||||
//add new sprite path to clone src
|
||||
expressionClone.attr('src', sprite.path);
|
||||
//add invisible clone to html
|
||||
expressionClone.appendTo($("#expression-holder"))
|
||||
|
||||
const duration = 200;
|
||||
|
||||
//add animation flags to both images
|
||||
//to prevent multiple expression changes happening simultaneously
|
||||
img.addClass('expression-animating');
|
||||
|
||||
// Set the parent container's min width and height before running the transition
|
||||
const imgWidth = img.width();
|
||||
const imgHeight = img.height();
|
||||
const expressionHolder = img.parent();
|
||||
expressionHolder.css('min-width', imgWidth > 100 ? imgWidth : 100);
|
||||
expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
|
||||
|
||||
//position absolute prevent the original from jumping around during transition
|
||||
img.css('position', 'absolute');
|
||||
expressionClone.addClass('expression-animating');
|
||||
//fade the clone in
|
||||
expressionClone.css({
|
||||
opacity: 0
|
||||
}).animate({
|
||||
opacity: 1
|
||||
}, duration)
|
||||
//when finshed fading in clone, fade out the original
|
||||
.promise().done(function () {
|
||||
img.animate({
|
||||
opacity: 0
|
||||
}, duration);
|
||||
//remove old expression
|
||||
img.remove();
|
||||
//replace ID so it becomes the new 'original' expression for next change
|
||||
expressionClone.attr('id', 'expression-image');
|
||||
expressionClone.removeClass('expression-animating');
|
||||
|
||||
// Reset the expression holder min height and width
|
||||
expressionHolder.css('min-width', 100);
|
||||
expressionHolder.css('min-height', 100);
|
||||
});
|
||||
|
||||
|
||||
expressionClone.removeClass('expression-clone');
|
||||
|
||||
expressionClone.removeClass('default');
|
||||
expressionClone.off('error');
|
||||
expressionClone.on('error', function () {
|
||||
console.debug('Expression image error', sprite.path);
|
||||
$(this).attr('src', '');
|
||||
$(this).off('error');
|
||||
if (force && extension_settings.expressions.showDefault) {
|
||||
setDefault();
|
||||
}
|
||||
});
|
||||
function setDefault() {
|
||||
console.debug('setting default');
|
||||
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
||||
//console.log(defImgUrl);
|
||||
img.attr('src', defImgUrl);
|
||||
img.addClass('default');
|
||||
}
|
||||
document.getElementById("expression-holder").style.display = '';
|
||||
|
||||
} else {
|
||||
if (extension_settings.expressions.showDefault) {
|
||||
setDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function setDefault() {
|
||||
console.debug('setting default');
|
||||
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
||||
console.log(defImgUrl);
|
||||
img.attr('src', defImgUrl);
|
||||
img.addClass('default');
|
||||
|
||||
live2dcheck().then(result => {
|
||||
if (result) {
|
||||
// Find the <img> element with id="expression-image" and class="expression"
|
||||
const imgElement = document.querySelector('img#expression-image.expression');
|
||||
//console.log("searching");
|
||||
if (imgElement) {
|
||||
//console.log("setting value");
|
||||
imgElement.src = getApiUrl() + '/api/live2d/result_feed';
|
||||
}
|
||||
|
||||
} else {
|
||||
//console.log("The fetch failed!");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
document.getElementById("expression-holder").style.display = '';
|
||||
}
|
||||
|
||||
function onClickExpressionImage() {
|
||||
@@ -1052,7 +1245,6 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
$('body').append(element);
|
||||
}
|
||||
function addSettings() {
|
||||
|
||||
const html = `
|
||||
<div class="expression_settings">
|
||||
<div class="inline-drawer">
|
||||
@@ -1060,8 +1252,16 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
<b>Character Expressions</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
|
||||
<div class="inline-drawer-content">
|
||||
<div class="offline_mode">
|
||||
<!-- Toggle button for aituber/static images -->
|
||||
<div class="toggle_button">
|
||||
<label class="switch">
|
||||
<input id="image_type_toggle" type="checkbox">
|
||||
<span class="slider round"></span>
|
||||
<label for="image_type_toggle">Image Type - Live2d (extras)</label>
|
||||
</div>
|
||||
<div class="offline_mode">
|
||||
<small>You are in offline mode. Click on the image below to set the expression.</small>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
@@ -1090,6 +1290,7 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#extensions_settings').append(html);
|
||||
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
|
||||
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
|
||||
@@ -1105,6 +1306,10 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
||||
$(window).on("resize", updateVisualNovelModeDebounced);
|
||||
$('.expression_settings').hide();
|
||||
|
||||
$('#image_type_toggle').on('click', function () {
|
||||
setLive2dState(this.checked);
|
||||
});
|
||||
}
|
||||
|
||||
addExpressionImage();
|
||||
|
@@ -544,6 +544,30 @@ async function onSelectInjectFile(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the length of character description in the current context
|
||||
function getCharacterDataLength() {
|
||||
const context = getContext();
|
||||
const character = context.characters[context.characterId];
|
||||
|
||||
if (typeof character?.data !== 'object') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let characterDataLength = 0;
|
||||
|
||||
for (const [key, value] of Object.entries(character.data)) {
|
||||
if (typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (['description', 'personality', 'scenario'].includes(key)) {
|
||||
characterDataLength += character.data[key].length;
|
||||
}
|
||||
}
|
||||
|
||||
return characterDataLength;
|
||||
}
|
||||
|
||||
/*
|
||||
* Automatically adjusts the extension settings for the optimal number of messages to keep and query based
|
||||
* on the chat history and a specified maximum context length.
|
||||
@@ -551,13 +575,17 @@ 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;
|
||||
}
|
||||
|
||||
// Adjust max context for character defs length
|
||||
maxContext = Math.floor(maxContext - (getCharacterDataLength() / CHARACTERS_PER_TOKEN_RATIO));
|
||||
console.debug('CHROMADB: Max context adjusted for character defs: %o', maxContext);
|
||||
|
||||
console.debug('CHROMADB: Mean message length (characters): %o', meanMessageLength);
|
||||
// Convert to number of "tokens"
|
||||
const meanMessageLengthTokens = Math.ceil(meanMessageLength / CHARACTERS_PER_TOKEN_RATIO);
|
||||
@@ -611,7 +639,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 +839,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 +862,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>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { getStringHash, debounce, waitUntilCondition } from "../../utils.js";
|
||||
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from "../../utils.js";
|
||||
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
|
||||
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js";
|
||||
import { is_group_generating, selected_group } from "../../group-chats.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '1_memory';
|
||||
@@ -12,7 +13,21 @@ let lastMessageHash = null;
|
||||
let lastMessageId = null;
|
||||
let inApiCall = false;
|
||||
|
||||
const formatMemoryValue = (value) => value ? `Summary: ${value.trim()}` : '';
|
||||
const formatMemoryValue = function (value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
if (extension_settings.memory.template) {
|
||||
let result = extension_settings.memory.template.replace(/{{summary}}/i, value);
|
||||
return substituteParams(result);
|
||||
} else {
|
||||
return `Summary: ${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
const saveChatDebounced = debounce(() => getContext().saveChat(), 2000);
|
||||
|
||||
const summary_sources = {
|
||||
@@ -21,6 +36,7 @@ const summary_sources = {
|
||||
};
|
||||
|
||||
const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events that have happened in the chat so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]';
|
||||
const defaultTemplate = '[Summary: {{summary}}]';
|
||||
|
||||
const defaultSettings = {
|
||||
minLongMemory: 16,
|
||||
@@ -46,6 +62,9 @@ const defaultSettings = {
|
||||
memoryFrozen: false,
|
||||
source: summary_sources.extras,
|
||||
prompt: defaultPrompt,
|
||||
template: defaultTemplate,
|
||||
position: extension_prompt_types.AFTER_SCENARIO,
|
||||
depth: 2,
|
||||
promptWords: 200,
|
||||
promptMinWords: 25,
|
||||
promptMaxWords: 1000,
|
||||
@@ -54,6 +73,10 @@ const defaultSettings = {
|
||||
promptMinInterval: 1,
|
||||
promptMaxInterval: 100,
|
||||
promptIntervalStep: 1,
|
||||
promptForceWords: 0,
|
||||
promptForceWordsStep: 100,
|
||||
promptMinForceWords: 0,
|
||||
promptMaxForceWords: 10000,
|
||||
};
|
||||
|
||||
function loadSettings() {
|
||||
@@ -61,20 +84,10 @@ function loadSettings() {
|
||||
Object.assign(extension_settings.memory, defaultSettings);
|
||||
}
|
||||
|
||||
if (extension_settings.memory.source === undefined) {
|
||||
extension_settings.memory.source = defaultSettings.source;
|
||||
}
|
||||
|
||||
if (extension_settings.memory.prompt === undefined) {
|
||||
extension_settings.memory.prompt = defaultSettings.prompt;
|
||||
}
|
||||
|
||||
if (extension_settings.memory.promptWords === undefined) {
|
||||
extension_settings.memory.promptWords = defaultSettings.promptWords;
|
||||
}
|
||||
|
||||
if (extension_settings.memory.promptInterval === undefined) {
|
||||
extension_settings.memory.promptInterval = defaultSettings.promptInterval;
|
||||
for (const key of Object.keys(defaultSettings)) {
|
||||
if (extension_settings.memory[key] === undefined) {
|
||||
extension_settings.memory[key] = defaultSettings[key];
|
||||
}
|
||||
}
|
||||
|
||||
$('#summary_source').val(extension_settings.memory.source).trigger('change');
|
||||
@@ -87,6 +100,10 @@ function loadSettings() {
|
||||
$('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
|
||||
$('#memory_prompt_words').val(extension_settings.memory.promptWords).trigger('input');
|
||||
$('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
|
||||
$('#memory_template').val(extension_settings.memory.template).trigger('input');
|
||||
$('#memory_depth').val(extension_settings.memory.depth).trigger('input');
|
||||
$(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
|
||||
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
|
||||
}
|
||||
|
||||
function onSummarySourceChange(event) {
|
||||
@@ -170,6 +187,31 @@ function onMemoryPromptInput() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryTemplateInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.template = value;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryDepthInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.depth = Number(value);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryPositionChange(e) {
|
||||
const value = e.target.value;
|
||||
extension_settings.memory.position = value;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryPromptWordsForceInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.promptForceWords = Number(value);
|
||||
$('#memory_prompt_words_force_value').text(extension_settings.memory.promptForceWords);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function saveLastValues() {
|
||||
const context = getContext();
|
||||
lastGroupId = context.groupId;
|
||||
@@ -292,8 +334,12 @@ async function summarizeChat(context) {
|
||||
|
||||
async function summarizeChatMain(context, force) {
|
||||
try {
|
||||
// Wait for group to finish generating
|
||||
if (selected_group) {
|
||||
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
|
||||
}
|
||||
// Wait for the send button to be released
|
||||
waitUntilCondition(() => is_send_press === false, 10000, 100);
|
||||
waitUntilCondition(() => is_send_press === false, 30000, 100);
|
||||
} catch {
|
||||
console.debug('Timeout waiting for is_send_press');
|
||||
return;
|
||||
@@ -310,19 +356,30 @@ async function summarizeChatMain(context, force) {
|
||||
}
|
||||
|
||||
let messagesSinceLastSummary = 0;
|
||||
let wordsSinceLastSummary = 0;
|
||||
let conditionSatisfied = false;
|
||||
for (let i = context.chat.length - 1; i >= 0; i--) {
|
||||
if (context.chat[i].extra && context.chat[i].extra.memory) {
|
||||
break;
|
||||
}
|
||||
messagesSinceLastSummary++;
|
||||
wordsSinceLastSummary += extractAllWords(context.chat[i].mes).length;
|
||||
}
|
||||
|
||||
if (messagesSinceLastSummary < extension_settings.memory.promptInterval && !force) {
|
||||
console.debug(`Not enough messages since last summary (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}`);
|
||||
if (messagesSinceLastSummary >= extension_settings.memory.promptInterval) {
|
||||
conditionSatisfied = true;
|
||||
}
|
||||
|
||||
if (extension_settings.memory.promptForceWords && wordsSinceLastSummary >= extension_settings.memory.promptForceWords) {
|
||||
conditionSatisfied = true;
|
||||
}
|
||||
|
||||
if (!conditionSatisfied && !force) {
|
||||
console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary);
|
||||
console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
|
||||
const prompt = substituteParams(extension_settings.memory.prompt)
|
||||
.replace(/{{words}}/gi, extension_settings.memory.promptWords);
|
||||
|
||||
@@ -458,9 +515,11 @@ function onMemoryContentInput() {
|
||||
|
||||
function setMemoryContext(value, saveToMessage) {
|
||||
const context = getContext();
|
||||
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_prompt_types.AFTER_SCENARIO);
|
||||
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth);
|
||||
$('#memory_contents').val(value);
|
||||
console.log('Memory set to: ' + value);
|
||||
console.log('Summary set to: ' + value);
|
||||
console.debug('Position: ' + extension_settings.memory.position);
|
||||
console.debug('Depth: ' + extension_settings.memory.depth);
|
||||
|
||||
if (saveToMessage && context.chat.length) {
|
||||
const idx = context.chat.length - 2;
|
||||
@@ -496,6 +555,21 @@ jQuery(function () {
|
||||
<input id="memory_restore" class="menu_button" type="button" value="Restore previous state" />
|
||||
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Pause summarization</label>
|
||||
</div>
|
||||
<div class="memory_template">
|
||||
<label for="memory_template">Injection template:</label>
|
||||
<textarea id="memory_template" class="text_pole textarea_compact" rows="1" placeholder="Use {{summary}} macro to specify the position of summarized text."></textarea>
|
||||
</div>
|
||||
<label for="memory_position">Injection position:</label>
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
<input type="radio" name="memory_position" value="0" />
|
||||
After scenario
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="memory_position" value="1" />
|
||||
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
|
||||
</label>
|
||||
</div>
|
||||
<div data-source="main" class="memory_contents_controls">
|
||||
</div>
|
||||
<div data-source="main">
|
||||
@@ -511,6 +585,9 @@ jQuery(function () {
|
||||
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
|
||||
<label for="memory_prompt_interval">Update interval (<span id="memory_prompt_interval_value"></span> messages)</label>
|
||||
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
|
||||
<label for="memory_prompt_words_force">Force update after (<span id="memory_prompt_words_force_value"></span> words)</label>
|
||||
<small>Set to 0 to disable</small>
|
||||
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
|
||||
</div>
|
||||
<div data-source="extras">
|
||||
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
|
||||
@@ -542,6 +619,10 @@ jQuery(function () {
|
||||
$('#memory_prompt_interval').on('input', onMemoryPromptIntervalInput);
|
||||
$('#memory_prompt').on('input', onMemoryPromptInput);
|
||||
$('#memory_force_summarize').on('click', forceSummarizeChat);
|
||||
$('#memory_template').on('input', onMemoryTemplateInput);
|
||||
$('#memory_depth').on('input', onMemoryDepthInput);
|
||||
$('input[name="memory_position"]').on('change', onMemoryPositionChange);
|
||||
$('#memory_prompt_words_force').on('input', onMemoryPromptWordsForceInput);
|
||||
}
|
||||
|
||||
addExtensionControls();
|
||||
|
@@ -1,34 +1,26 @@
|
||||
#memory_settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#memory_settings textarea {
|
||||
font-size: calc(var(--mainFontSize) * 0.9);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
#memory_settings input[type="range"] {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#memory_settings label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label[for="memory_frozen"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
label[for="memory_frozen"] input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.memory_contents_controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#memory_settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#memory_settings textarea {
|
||||
font-size: calc(var(--mainFontSize) * 0.9);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
label[for="memory_frozen"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
label[for="memory_frozen"] input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.memory_contents_controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { chat_metadata } from "../../../script.js";
|
||||
import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } from "../../../script.js";
|
||||
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
|
||||
import {
|
||||
substituteParams,
|
||||
@@ -7,97 +7,103 @@ import {
|
||||
generateQuietPrompt,
|
||||
} from "../../../script.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { waitUntilCondition } from "../../utils.js";
|
||||
import { is_group_generating, selected_group } from "../../group-chats.js";
|
||||
|
||||
const MODULE_NAME = "Objective"
|
||||
|
||||
|
||||
let globalObjective = ""
|
||||
let taskTree = null
|
||||
let globalTasks = []
|
||||
let currentChatId = ""
|
||||
let currentObjective = null
|
||||
let currentTask = null
|
||||
let checkCounter = 0
|
||||
let lastMessageWasSwipe = false
|
||||
|
||||
|
||||
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)
|
||||
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;
|
||||
}
|
||||
|
||||
globalTasks.splice(index, 1)
|
||||
setCurrentTask()
|
||||
updateUiTaskList()
|
||||
function substituteParamsPrompts(content) {
|
||||
content = content.replace(/{{objective}}/gi, currentObjective.description)
|
||||
content = content.replace(/{{task}}/gi, currentTask.description)
|
||||
if (currentTask.parent){
|
||||
content = content.replace(/{{parent}}/gi, currentTask.parent.description)
|
||||
}
|
||||
content = substituteParams(content)
|
||||
return content
|
||||
}
|
||||
|
||||
// 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 = substituteParamsPrompts(objectivePrompts.createTask);
|
||||
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)} `)
|
||||
toastr.success(`Generated ${globalTasks.length} tasks`, 'Done!');
|
||||
updateUiTaskList();
|
||||
setCurrentTask();
|
||||
console.info(`Response for Objective: '${currentObjective.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(currentObjective.children.map(v => {return v.toSaveState()}), null, 2)} `)
|
||||
toastr.success(`Generated ${currentObjective.children.length} tasks`, 'Done!');
|
||||
}
|
||||
|
||||
// Call Quiet Generate to check if a task is completed
|
||||
@@ -106,14 +112,28 @@ async function checkTaskCompleted() {
|
||||
if (jQuery.isEmptyObject(currentTask)) {
|
||||
return
|
||||
}
|
||||
checkCounter = $('#objective-check-frequency').val()
|
||||
|
||||
const prompt = substituteParams(objectivePrompts["checkTaskCompleted"].replace(/{{task}}/gi, currentTask.description));
|
||||
try {
|
||||
// Wait for group to finish generating
|
||||
if (selected_group) {
|
||||
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
|
||||
}
|
||||
// Another extension might be doing something with the chat, so wait for it to finish
|
||||
await waitUntilCondition(() => is_send_press === false, 30000, 10);
|
||||
} catch {
|
||||
console.debug("Failed to wait for group to finish generating")
|
||||
return;
|
||||
}
|
||||
|
||||
checkCounter = $('#objective-check-frequency').val()
|
||||
toastr.info("Checking for task completion.")
|
||||
|
||||
const prompt = substituteParamsPrompts(objectivePrompts.checkTaskCompleted);
|
||||
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 +142,57 @@ async function checkTaskCompleted() {
|
||||
}
|
||||
}
|
||||
|
||||
function getNextIncompleteTaskRecurse(task){
|
||||
if (task.completed === false // Return task if incomplete
|
||||
&& task.children.length === 0 // Ensure task has no children, it's subtasks will determine completeness
|
||||
&& task.parentId !== "" // Must have parent id. Only root task will be missing this and we dont want that
|
||||
){
|
||||
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 = substituteParamsPrompts(objectivePrompts.currentTask);
|
||||
|
||||
// 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 +203,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 +232,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 +302,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 +316,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,56 +333,206 @@ 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">
|
||||
<small>Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables. Save template to persist changes.</small>
|
||||
<br>
|
||||
<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">
|
||||
<label for="objective-custom-prompt-select">Custom Prompt Select</label>
|
||||
<select id="objective-custom-prompt-select"><select>
|
||||
</div>
|
||||
<div class="objective_prompt_block">
|
||||
<input id="objective-custom-prompt-new" class="menu_button" type="submit" value="New Prompt" />
|
||||
<input id="objective-custom-prompt-save" class="menu_button" type="submit" value="Save Prompt" />
|
||||
<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 new
|
||||
$('#objective-custom-prompt-new').on('click', () => {
|
||||
newCustomPrompt()
|
||||
})
|
||||
|
||||
// Handle save
|
||||
$('#objective-custom-prompt-save').on('click', () => {
|
||||
saveCustomPrompt()
|
||||
})
|
||||
|
||||
// Handle delete
|
||||
$('#objective-custom-prompt-delete').on('click', () => {
|
||||
deleteCustomPrompt()
|
||||
})
|
||||
|
||||
// Handle load
|
||||
$('#objective-custom-prompt-select').on('change', loadCustomPrompt)
|
||||
}
|
||||
async function newCustomPrompt() {
|
||||
const customPromptName = await callPopup('<h3>Custom Prompt name:</h3>', 'input');
|
||||
|
||||
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], objectivePrompts)
|
||||
saveSettingsDebounced()
|
||||
populateCustomPrompts()
|
||||
}
|
||||
|
||||
function saveCustomPrompt() {
|
||||
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
|
||||
if (customPromptName == "default"){
|
||||
toastr.error("Cannot save over default prompt")
|
||||
return
|
||||
}
|
||||
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
|
||||
saveSettingsDebounced()
|
||||
populateCustomPrompts()
|
||||
}
|
||||
|
||||
function deleteCustomPrompt(){
|
||||
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
|
||||
|
||||
if (customPromptName == "default"){
|
||||
toastr.error("Cannot delete default prompt")
|
||||
return
|
||||
}
|
||||
delete extension_settings.objective.customPrompts[customPromptName]
|
||||
saveSettingsDebounced()
|
||||
populateCustomPrompts()
|
||||
loadCustomPrompt()
|
||||
}
|
||||
|
||||
function loadCustomPrompt(){
|
||||
const optionSelected = $("#objective-custom-prompt-select").find(':selected').val()
|
||||
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-custom-prompt-select').empty()
|
||||
for (const customPromptName in extension_settings.objective.customPrompts){
|
||||
const option = document.createElement('option');
|
||||
option.innerText = customPromptName;
|
||||
option.value = customPromptName;
|
||||
option.selected = customPromptName
|
||||
$('#objective-custom-prompt-select').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.
|
||||
function resetState() {
|
||||
lastMessageWasSwipe = false
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
@@ -288,15 +544,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 +559,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 +574,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 +596,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 +621,13 @@ function onChatDepthInput() {
|
||||
setCurrentTask() // Ensure extension prompt is updated
|
||||
}
|
||||
|
||||
function onObjectiveTextFocusOut(){
|
||||
if (currentObjective){
|
||||
currentObjective.description = $('#objective-text').val()
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
|
||||
// Update how often we check for task completion
|
||||
function onCheckFrequencyInput() {
|
||||
checkCounter = $("#objective-check-frequency").val()
|
||||
@@ -364,10 +640,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 +674,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 +683,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 +746,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,14 +791,21 @@ 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, () => {
|
||||
resetState()
|
||||
});
|
||||
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, () => {
|
||||
lastMessageWasSwipe = true
|
||||
})
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, () => {
|
||||
if (currentChatId == undefined) {
|
||||
if (currentChatId == undefined || jQuery.isEmptyObject(currentTask) || lastMessageWasSwipe) {
|
||||
lastMessageWasSwipe = false
|
||||
return
|
||||
}
|
||||
if ($("#objective-check-frequency").val() > 0) {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -1,19 +1,49 @@
|
||||
import { saveSettingsDebounced } from "../../../script.js";
|
||||
import { saveSettingsDebounced, callPopup, getRequestHeaders } from "../../../script.js";
|
||||
import { getContext, extension_settings } from "../../extensions.js";
|
||||
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
|
||||
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "../../slash-commands.js";
|
||||
|
||||
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'quick-reply';
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
let presets = [];
|
||||
let selected_preset = '';
|
||||
|
||||
const defaultSettings = {
|
||||
quickReplyEnabled: false,
|
||||
quickReplyEnabled: true,
|
||||
numberOfSlots: 5,
|
||||
quickReplySlots: [],
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
//method from worldinfo
|
||||
async function updateQuickReplyPresetList() {
|
||||
var result = await fetch("/getsettings", {
|
||||
method: "POST",
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
var data = await result.json();
|
||||
presets = data.quickReplyPresets?.length ? data.quickReplyPresets : [];
|
||||
console.log(presets)
|
||||
$("#quickReplyPresets").find('option[value!=""]').remove();
|
||||
|
||||
|
||||
if (presets !== undefined) {
|
||||
presets.forEach((item, i) => {
|
||||
$("#quickReplyPresets").append(`<option value='${item.name}'${selected_preset.includes(item.name) ? ' selected' : ''}>${item.name}</option>`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings(type) {
|
||||
if (type === 'init') {
|
||||
await updateQuickReplyPresetList()
|
||||
}
|
||||
if (Object.keys(extension_settings.quickReply).length === 0) {
|
||||
Object.assign(extension_settings.quickReply, defaultSettings);
|
||||
}
|
||||
@@ -111,6 +141,51 @@ async function moduleWorker() {
|
||||
if (extension_settings.quickReply.quickReplyEnabled === true) {
|
||||
$('#quickReplyBar').toggle(getContext().onlineStatus !== 'no_connection');
|
||||
}
|
||||
if (extension_settings.quickReply.selectedPreset) {
|
||||
selected_preset = extension_settings.quickReply.selectedPreset;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveQuickReplyPreset() {
|
||||
const name = await callPopup('Enter a name for the Quick Reply Preset:', 'input');
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quickReplyPreset = {
|
||||
name: name,
|
||||
quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled,
|
||||
quickReplySlots: extension_settings.quickReply.quickReplySlots,
|
||||
numberOfSlots: extension_settings.quickReply.numberOfSlots,
|
||||
selectedPreset: name
|
||||
}
|
||||
|
||||
const response = await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const quickReplyPresetIndex = presets.findIndex(x => x.name == name);
|
||||
|
||||
if (quickReplyPresetIndex == -1) {
|
||||
presets.push(quickReplyPreset);
|
||||
const option = document.createElement('option');
|
||||
option.selected = true;
|
||||
option.value = name;
|
||||
option.innerText = name;
|
||||
$('#quickReplyPresets').append(option);
|
||||
}
|
||||
else {
|
||||
presets[quickReplyPresetIndex] = quickReplyPreset;
|
||||
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
} else {
|
||||
toastr.warning('Failed to save Quick Reply Preset.')
|
||||
}
|
||||
}
|
||||
|
||||
async function onQuickReplyNumberOfSlotsInput() {
|
||||
@@ -178,6 +253,45 @@ function generateQuickReplyElements() {
|
||||
});
|
||||
}
|
||||
|
||||
async function applyQuickReplyPreset(name) {
|
||||
const quickReplyPreset = presets.find(x => x.name == name);
|
||||
|
||||
if (!quickReplyPreset) {
|
||||
toastr.warning(`error, QR preset '${name}' not found. Confirm you are using proper case sensitivity!`)
|
||||
return;
|
||||
}
|
||||
|
||||
extension_settings.quickReply = quickReplyPreset;
|
||||
extension_settings.quickReply.selectedPreset = name;
|
||||
saveSettingsDebounced()
|
||||
loadSettings('init')
|
||||
addQuickReplyBar();
|
||||
moduleWorker();
|
||||
|
||||
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
|
||||
console.debug('QR Preset applied: ' + name);
|
||||
}
|
||||
|
||||
async function doQRPresetSwitch(_, text) {
|
||||
text = String(text)
|
||||
applyQuickReplyPreset(text)
|
||||
}
|
||||
|
||||
async function doQR(_, text) {
|
||||
if (!text) {
|
||||
toastr.warning('must specify which QR # to use')
|
||||
return
|
||||
}
|
||||
|
||||
text = Number(text)
|
||||
//use scale starting with 0
|
||||
//ex: user inputs "/qr 2" >> qr with data-index 1 (but 2nd item displayed) gets triggered
|
||||
let QRnum = Number(text - 1)
|
||||
if (QRnum <= 0) { QRnum = 0 }
|
||||
const whichQR = $("#quickReplies").find(`[data-index='${QRnum}']`);
|
||||
whichQR.trigger('click')
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
|
||||
moduleWorker();
|
||||
@@ -190,11 +304,18 @@ jQuery(async () => {
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label class="checkbox_label marginBot10">
|
||||
<input id="quickReplyEnabled" type="checkbox" />
|
||||
Enable Quick Replies
|
||||
</label>
|
||||
<label for="quickReplyNumberOfSlots">Number of slots:</label>
|
||||
<div class="flex-container ">
|
||||
<label class="checkbox_label marginBot10 wide100p flexnowrap">
|
||||
<input id="quickReplyEnabled" type="checkbox" />
|
||||
Enable Quick Replies
|
||||
</label>
|
||||
<div class="flex-container flexnowrap wide100p">
|
||||
<select id="quickReplyPresets" name="quickreply-preset">
|
||||
</select>
|
||||
<i id="quickReplyPresetSaveButton" class="fa-solid fa-save"></i>
|
||||
</div>
|
||||
<label for="quickReplyNumberOfSlots">Number of slots:</label>
|
||||
</div>
|
||||
<div class="flex-container flexGap5 flexnowrap">
|
||||
<input id="quickReplyNumberOfSlots" class="text_pole" type="number" min="1" max="100" value="" />
|
||||
<div class="menu_button menu_button_icon" id="quickReplyNumberOfSlotsApply">
|
||||
@@ -212,8 +333,23 @@ jQuery(async () => {
|
||||
|
||||
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
|
||||
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
|
||||
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
|
||||
|
||||
await loadSettings();
|
||||
$("#quickReplyPresets").on('change', async function () {
|
||||
const quickReplyPresetSelected = $(this).find(':selected').val();
|
||||
extension_settings.quickReplyPreset = quickReplyPresetSelected;
|
||||
applyQuickReplyPreset(quickReplyPresetSelected);
|
||||
saveSettingsDebounced();
|
||||
|
||||
});
|
||||
|
||||
await loadSettings('init');
|
||||
addQuickReplyBar();
|
||||
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
registerSlashCommand('qr', doQR, [], "- requires number argument, activates the specified QuickReply", true, true);
|
||||
registerSlashCommand('qrset', doQRPresetSwitch, [], "- arg: QuickReply Preset Name, swaps to that QR preset", true, true);
|
||||
|
||||
})
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
233
public/scripts/extensions/speech-recognition/browser.js
Normal file
233
public/scripts/extensions/speech-recognition/browser.js
Normal 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")
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,110 +1,439 @@
|
||||
// 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'
|
||||
import { StreamingSttProvider } from './streaming.js'
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'Speech Recognition';
|
||||
const DEBUG_PREFIX = "<Speech Recognition module> "
|
||||
const UPDATE_INTERVAL = 100;
|
||||
|
||||
let inApiCall = false;
|
||||
|
||||
let sttProviders = {
|
||||
None: null,
|
||||
Browser: BrowserSttProvider,
|
||||
Whisper: WhisperSttProvider,
|
||||
Vosk: VoskSttProvider,
|
||||
Streaming: StreamingSttProvider,
|
||||
}
|
||||
|
||||
let sttProvider = null
|
||||
let sttProviderName = "None"
|
||||
|
||||
let audioRecording = false
|
||||
const constraints = { audio: { sampleSize: 16, channelCount: 1, sampleRate: 16000 } };
|
||||
let audioChunks = [];
|
||||
|
||||
async function moduleWorker() {
|
||||
if (sttProviderName != "Streaming") {
|
||||
return;
|
||||
}
|
||||
|
||||
// API is busy
|
||||
if (inApiCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
inApiCall = true;
|
||||
const userMessageOriginal = await sttProvider.getUserMessage();
|
||||
let userMessageFormatted = userMessageOriginal.trim();
|
||||
|
||||
if (userMessageFormatted.length > 0)
|
||||
{
|
||||
console.debug(DEBUG_PREFIX+"recorded transcript: \""+userMessageFormatted+"\"");
|
||||
|
||||
let userMessageLower = userMessageFormatted.toLowerCase();
|
||||
// remove punctuation
|
||||
let userMessageRaw = userMessageLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
|
||||
|
||||
console.debug(DEBUG_PREFIX+"raw transcript:",userMessageRaw);
|
||||
|
||||
// Detect trigger words
|
||||
let messageStart = -1;
|
||||
|
||||
if (extension_settings.speech_recognition.Streaming.triggerWordsEnabled) {
|
||||
|
||||
for (const triggerWord of extension_settings.speech_recognition.Streaming.triggerWords) {
|
||||
const triggerPos = userMessageRaw.indexOf(triggerWord.toLowerCase());
|
||||
|
||||
// Trigger word not found or not starting message and just a substring
|
||||
if (triggerPos == -1){ // | (triggerPos > 0 & userMessageFormatted[triggerPos-1] != " ")) {
|
||||
console.debug(DEBUG_PREFIX+"trigger word not found: ", triggerWord);
|
||||
}
|
||||
else {
|
||||
console.debug(DEBUG_PREFIX+"Found trigger word: ", triggerWord, " at index ", triggerPos);
|
||||
if (triggerPos < messageStart | messageStart == -1) { // & (triggerPos + triggerWord.length) < userMessageFormatted.length)) {
|
||||
messageStart = triggerPos; // + triggerWord.length + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageStart = 0;
|
||||
}
|
||||
|
||||
if (messageStart == -1) {
|
||||
console.debug(DEBUG_PREFIX+"message ignored, no trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"");
|
||||
if (extension_settings.speech_recognition.Streaming.debug) {
|
||||
toastr.info(
|
||||
"No trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"",
|
||||
DEBUG_PREFIX+"message ignored.",
|
||||
{ timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
else{
|
||||
userMessageFormatted = userMessageFormatted.substring(messageStart);
|
||||
processTranscript(userMessageFormatted);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug(DEBUG_PREFIX+"Received empty transcript, ignored");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error);
|
||||
}
|
||||
finally {
|
||||
inApiCall = false;
|
||||
}
|
||||
return interimTranscript;
|
||||
}
|
||||
|
||||
function composeValues(previous, interim) {
|
||||
let spacing = '';
|
||||
if (previous.endsWith('.')) spacing = ' ';
|
||||
return previous + spacing + interim;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
(function ($) {
|
||||
$.fn.speechRecognitionPlugin = function (options) {
|
||||
const settings = $.extend({
|
||||
grammar: '' // Custom grammar
|
||||
}, options);
|
||||
|
||||
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const speechRecognitionList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
|
||||
|
||||
if (!speechRecognition) {
|
||||
console.warn('Speech recognition is not supported in this browser.');
|
||||
return;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const recognition = new speechRecognition();
|
||||
|
||||
if (settings.grammar && speechRecognitionList) {
|
||||
speechRecognitionList.addFromString(settings.grammar, 1);
|
||||
recognition.grammars = speechRecognitionList;
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// TODO: This should be configurable.
|
||||
recognition.lang = 'en-US'; // Set the language to English (US).
|
||||
//##############//
|
||||
// STT Provider //
|
||||
//##############//
|
||||
|
||||
const $textarea = this;
|
||||
const $button = $('<div class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
|
||||
function loadSttProvider(provider) {
|
||||
//Clear the current config and add new config
|
||||
$("#speech_recognition_provider_settings").html("");
|
||||
|
||||
// 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]);
|
||||
$("#microphone_button").show();
|
||||
}
|
||||
|
||||
if (sttProviderName == "Vosk" | sttProviderName == "Whisper") {
|
||||
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
|
||||
loadNavigatorAudioRecording();
|
||||
$("#microphone_button").show();
|
||||
}
|
||||
|
||||
if (sttProviderName == "Streaming") {
|
||||
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
|
||||
$("#microphone_button").off('click');
|
||||
$("#microphone_button").hide();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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]]);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
async function onMessageMappingEnabledClick() {
|
||||
extension_settings.speech_recognition.messageMappingEnabled = $('#speech_recognition_message_mapping_enabled').is(':checked');
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
$(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;
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
addExtensionControls(); // No init dependencies
|
||||
loadSettings(); // Depends on Extension Controls and loadTtsProvider
|
||||
loadSttProvider(extension_settings.speech_recognition.currentProvider); // No dependencies
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
|
||||
moduleWorker();
|
||||
})
|
||||
|
@@ -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"
|
||||
}
|
||||
|
102
public/scripts/extensions/speech-recognition/streaming.js
Normal file
102
public/scripts/extensions/speech-recognition/streaming.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { getApiUrl, doExtrasFetch, modules } from "../../extensions.js";
|
||||
export { StreamingSttProvider }
|
||||
|
||||
const DEBUG_PREFIX = "<Speech Recognition module (streaming)> "
|
||||
|
||||
class StreamingSttProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
|
||||
defaultSettings = {
|
||||
triggerWordsText: "",
|
||||
triggerWords : [],
|
||||
triggerWordsEnabled : false,
|
||||
debug : false,
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
let html = '\
|
||||
<div id="speech_recognition_streaming_trigger_words_div">\
|
||||
<span>Trigger words</span>\
|
||||
<textarea id="speech_recognition_streaming_trigger_words" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated words that triggers new message, example:\nhey, hey aqua, record, listen"></textarea>\
|
||||
<label class="checkbox_label" for="speech_recognition_streaming_trigger_words_enabled">\
|
||||
<input type="checkbox" id="speech_recognition_streaming_trigger_words_enabled" name="speech_recognition_trigger_words_enabled">\
|
||||
<small>Enable trigger words</small>\
|
||||
</label>\
|
||||
<label class="checkbox_label" for="speech_recognition_streaming_debug">\
|
||||
<input type="checkbox" id="speech_recognition_streaming_debug" name="speech_recognition_streaming_debug">\
|
||||
<small>Enable debug pop ups</small>\
|
||||
</label>\
|
||||
</div>\
|
||||
'
|
||||
return html
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
this.settings.triggerWordsText = $('#speech_recognition_streaming_trigger_words').val();
|
||||
let array = $('#speech_recognition_streaming_trigger_words').val().split(",");
|
||||
array = array.map(element => {return element.trim().toLowerCase();});
|
||||
array = array.filter((str) => str !== '');
|
||||
this.settings.triggerWords = array;
|
||||
this.settings.triggerWordsEnabled = $("#speech_recognition_streaming_trigger_words_enabled").is(':checked');
|
||||
this.settings.debug = $("#speech_recognition_streaming_debug").is(':checked');
|
||||
console.debug(DEBUG_PREFIX+" Updated settings: ", this.settings);
|
||||
this.loadSettings(this.settings);
|
||||
}
|
||||
|
||||
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}`
|
||||
}
|
||||
}
|
||||
|
||||
$("#speech_recognition_streaming_trigger_words").val(this.settings.triggerWordsText);
|
||||
$("#speech_recognition_streaming_trigger_words_enabled").prop('checked',this.settings.triggerWordsEnabled);
|
||||
$("#speech_recognition_streaming_debug").prop('checked',this.settings.debug);
|
||||
|
||||
console.debug(DEBUG_PREFIX+"streaming STT settings loaded")
|
||||
}
|
||||
|
||||
async getUserMessage() {
|
||||
// Return if module is not loaded
|
||||
if (!modules.includes('streaming-stt')) {
|
||||
console.debug(DEBUG_PREFIX+"Module streaming-stt must be activated in Sillytavern Extras for streaming user voice.")
|
||||
return "";
|
||||
}
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/speech-recognition/streaming/record-and-transcript';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify({ text: "" }),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX+'STT Generation Failed (streaming)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
const data = await apiResult.json();
|
||||
return data.transcript;
|
||||
}
|
||||
|
||||
}
|
65
public/scripts/extensions/speech-recognition/vosk.js
Normal file
65
public/scripts/extensions/speech-recognition/vosk.js
Normal 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;
|
||||
}
|
||||
}
|
67
public/scripts/extensions/speech-recognition/whisper.js
Normal file
67
public/scripts/extensions/speech-recognition/whisper.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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. 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');
|
||||
});
|
||||
|
@@ -24,6 +24,6 @@
|
||||
}
|
||||
|
||||
#sd_dropdown {
|
||||
z-index: 3000;
|
||||
z-index: 30000;
|
||||
backdrop-filter: blur(--SmartThemeBlurStrength);
|
||||
}
|
||||
}
|
||||
|
403
public/scripts/extensions/tts/coquitts.js
Normal file
403
public/scripts/extensions/tts/coquitts.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -1,19 +1,19 @@
|
||||
import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js'
|
||||
import { ModuleWorkerWrapper, extension_settings, getContext } from '../../extensions.js'
|
||||
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext } from '../../extensions.js'
|
||||
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'
|
||||
import { power_user } from '../../power-user.js'
|
||||
|
||||
const UPDATE_INTERVAL = 1000
|
||||
|
||||
let voiceMap = {} // {charName:voiceid, charName2:voiceid2}
|
||||
let audioControl
|
||||
|
||||
let storedvalue = false;
|
||||
let lastCharacterId = null
|
||||
let lastGroupId = null
|
||||
let lastChatId = null
|
||||
@@ -64,6 +64,7 @@ let ttsProviders = {
|
||||
ElevenLabs: ElevenLabsTtsProvider,
|
||||
Silero: SileroTtsProvider,
|
||||
System: SystemTtsProvider,
|
||||
Coqui: CoquiTtsProvider,
|
||||
Edge: EdgeTtsProvider,
|
||||
Novel: NovelTtsProvider,
|
||||
}
|
||||
@@ -162,6 +163,20 @@ async function moduleWorker() {
|
||||
ttsJobQueue.push(message)
|
||||
}
|
||||
|
||||
function talkingAnimation(switchValue) {
|
||||
const apiUrl = getApiUrl();
|
||||
const animationType = switchValue ? "start" : "stop";
|
||||
|
||||
if (switchValue !== storedvalue) {
|
||||
try {
|
||||
console.log(animationType + " Talking Animation");
|
||||
doExtrasFetch(`${apiUrl}/api/live2d/${animationType}_talking`);
|
||||
storedvalue = switchValue; // Update the storedvalue to the current switchValue
|
||||
} catch (error) {
|
||||
// Handle the error here or simply ignore it to prevent logging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetTtsPlayback() {
|
||||
// Stop system TTS utterance
|
||||
@@ -289,8 +304,10 @@ function updateUiAudioPlayState() {
|
||||
// Give user feedback that TTS is active by setting the stop icon if processing or playing
|
||||
if (!audioElement.paused || isTtsProcessing()) {
|
||||
img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton'
|
||||
talkingAnimation(true)
|
||||
} else {
|
||||
img = 'fa-solid fa-circle-play extensionsMenuExtensionButton'
|
||||
talkingAnimation(false)
|
||||
}
|
||||
$('#tts_media_control').attr('class', img);
|
||||
} else {
|
||||
@@ -352,6 +369,7 @@ async function processAudioJobQueue() {
|
||||
audioQueueProcessorReady = false
|
||||
currentAudioJob = audioJobQueue.pop()
|
||||
playAudioData(currentAudioJob)
|
||||
talkingAnimation(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
audioQueueProcessorReady = true
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
delay,
|
||||
isDataURL,
|
||||
createThumbnail,
|
||||
extractAllWords,
|
||||
} from './utils.js';
|
||||
import { RA_CountCharTokens, humanizedDateTime, dragElement } from "./RossAscends-mods.js";
|
||||
import { sortCharactersList, sortGroupMembers, loadMovingUIState } from './power-user.js';
|
||||
@@ -782,19 +783,6 @@ function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, i
|
||||
return memberIds;
|
||||
}
|
||||
|
||||
function extractAllWords(value) {
|
||||
const words = [];
|
||||
|
||||
if (!value) {
|
||||
return words;
|
||||
}
|
||||
|
||||
const matches = value.matchAll(/\b\w+\b/gim);
|
||||
for (let match of matches) {
|
||||
words.push(match[0].toLowerCase());
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
|
||||
async function deleteGroup(id) {
|
||||
|
@@ -4,6 +4,10 @@ import {
|
||||
getStoppingStrings,
|
||||
} from "../script.js";
|
||||
|
||||
import {
|
||||
power_user,
|
||||
} from "./power-user.js";
|
||||
|
||||
export {
|
||||
kai_settings,
|
||||
loadKoboldSettings,
|
||||
@@ -35,12 +39,12 @@ const MIN_STREAMING_KCPPVERSION = '1.30';
|
||||
function formatKoboldUrl(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
url.pathname = '/api';
|
||||
if (!power_user.relaxed_api_urls) {
|
||||
url.pathname = '/api';
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
} catch { } // Just using URL as a validation check
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadKoboldSettings(preset) {
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
getRequestHeaders,
|
||||
saveSettingsDebounced,
|
||||
getStoppingStrings,
|
||||
getTextTokens
|
||||
} from "../script.js";
|
||||
import { tokenizers } from "./power-user.js";
|
||||
|
||||
export {
|
||||
nai_settings,
|
||||
@@ -10,17 +13,25 @@ export {
|
||||
getNovelTier,
|
||||
};
|
||||
|
||||
const default_preamble = "[ Style: chat, complex, sensory, visceral ]";
|
||||
|
||||
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,
|
||||
nai_preamble: default_preamble,
|
||||
};
|
||||
|
||||
const nai_tiers = {
|
||||
@@ -45,13 +56,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 +77,92 @@ 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;
|
||||
if (settings.nai_preamble !== undefined) nai_settings.preamble = settings.nai_preamble;
|
||||
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 > 5) {
|
||||
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));
|
||||
$('#nai_preamble_textarea').val(ui_settings.nai_preamble);
|
||||
|
||||
$("#streaming_novel").prop('checked', ui_settings.streaming_novel);
|
||||
}
|
||||
|
||||
@@ -94,71 +171,134 @@ 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) {
|
||||
export function getNovelGenerationData(finalPromt, this_settings, this_amount_gen, isImpersonate) {
|
||||
const clio = nai_settings.model_novel.includes('clio');
|
||||
const kayra = nai_settings.model_novel.includes('kayra');
|
||||
const isNewModel = clio || kayra;
|
||||
|
||||
const tokenizerType = kayra ? tokenizers.NERD2 : (clio ? tokenizers.NERD : tokenizers.NONE);
|
||||
const stopSequences = (tokenizerType !== tokenizers.NONE)
|
||||
? getStoppingStrings(isImpersonate, false)
|
||||
.map(t => getTextTokens(tokenizerType, t))
|
||||
: undefined;
|
||||
|
||||
let useInstruct = false;
|
||||
if (isNewModel) {
|
||||
// NovelAI claims they scan backwards 1000 characters (not tokens!) to look for instruct brackets. That's really short.
|
||||
const tail = finalPromt.slice(-1500);
|
||||
useInstruct = tail.includes("}");
|
||||
}
|
||||
|
||||
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}},
|
||||
"stop_sequences": stopSequences,
|
||||
//bad_words_ids = {{50256}, {0}, {1}};
|
||||
"generate_until_sentence": true,
|
||||
"use_cache": false,
|
||||
"use_string": true,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"prefix": useInstruct ? "special_instruct" : (isNewModel ? "special_proseaugmenter" : "vanilla"),
|
||||
"order": this_settings.order,
|
||||
"streaming": nai_settings.streaming_novel,
|
||||
};
|
||||
@@ -206,13 +346,24 @@ export async function generateNovelWithStreaming(generate_data, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
$("#nai_preamble_textarea").on('input', function () {
|
||||
nai_settings.preamble = $('#nai_preamble_textarea').val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#nai_preamble_restore").on('click', function () {
|
||||
nai_settings.preamble = default_preamble;
|
||||
$('#nai_preamble_textarea').val(nai_settings.preamble);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
sliders.forEach(slider => {
|
||||
$(document).on("input", slider.sliderId, 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();
|
||||
});
|
||||
|
@@ -142,6 +142,8 @@ const default_settings = {
|
||||
max_context_unlocked: false,
|
||||
api_url_scale: '',
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
};
|
||||
|
||||
const oai_settings = {
|
||||
@@ -178,6 +180,8 @@ const oai_settings = {
|
||||
max_context_unlocked: false,
|
||||
api_url_scale: '',
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
};
|
||||
|
||||
let openai_setting_names;
|
||||
@@ -400,7 +404,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,11 +771,13 @@ 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) {
|
||||
generate_data['use_claude'] = true;
|
||||
generate_data['top_k'] = parseFloat(oai_settings.top_k_openai);
|
||||
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
|
||||
}
|
||||
|
||||
if (isOpenRouter) {
|
||||
@@ -1104,6 +1111,8 @@ 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;
|
||||
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
|
||||
|
||||
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 +1124,8 @@ 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);
|
||||
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
|
||||
|
||||
$('#model_openai_select').val(oai_settings.openai_model);
|
||||
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
|
||||
@@ -1168,9 +1179,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 +1220,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 +1320,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,
|
||||
@@ -1317,6 +1328,7 @@ async function saveOpenAIPreset(name, settings) {
|
||||
stream_openai: settings.stream_openai,
|
||||
api_url_scale: settings.api_url_scale,
|
||||
show_external_models: settings.show_external_models,
|
||||
assistant_prefill: settings.assistant_prefill,
|
||||
};
|
||||
|
||||
const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, {
|
||||
@@ -1649,6 +1661,8 @@ 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],
|
||||
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
|
||||
};
|
||||
|
||||
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
|
||||
@@ -1857,9 +1871,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 +1923,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 +1936,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 +2208,16 @@ $(document).ready(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#openai_proxy_password').on('input', function () {
|
||||
oai_settings.proxy_password = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_assistant_prefill').on('input', function () {
|
||||
oai_settings.assistant_prefill = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#api_button_openai").on("click", onConnectButtonClick);
|
||||
$("#openai_reverse_proxy").on("input", onReverseProxyInput);
|
||||
$("#model_openai_select").on("change", onModelChange);
|
||||
|
@@ -27,7 +27,7 @@ import {
|
||||
|
||||
import { registerSlashCommand } from "./slash-commands.js";
|
||||
|
||||
import { delay, debounce } from "./utils.js";
|
||||
import { delay } from "./utils.js";
|
||||
|
||||
export {
|
||||
loadPowerUserSettings,
|
||||
@@ -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 = {
|
||||
@@ -71,6 +71,7 @@ const tokenizers = {
|
||||
NERD: 4,
|
||||
NERD2: 5,
|
||||
API: 6,
|
||||
BEST_MATCH: 99,
|
||||
}
|
||||
|
||||
const send_on_enter_options = {
|
||||
@@ -87,7 +88,7 @@ export const persona_description_positions = {
|
||||
}
|
||||
|
||||
let power_user = {
|
||||
tokenizer: tokenizers.CLASSIC,
|
||||
tokenizer: tokenizers.BEST_MATCH,
|
||||
token_padding: 64,
|
||||
collapse_newlines: false,
|
||||
pygmalion_formatting: pygmalion_options.AUTO,
|
||||
@@ -163,6 +164,7 @@ let power_user = {
|
||||
prefer_character_jailbreak: true,
|
||||
continue_on_send: false,
|
||||
trim_spaces: true,
|
||||
relaxed_api_urls: false,
|
||||
|
||||
instruct: {
|
||||
enabled: false,
|
||||
@@ -176,6 +178,7 @@ let power_user = {
|
||||
preset: 'Alpaca',
|
||||
separator_sequence: '',
|
||||
macro: false,
|
||||
names_force_groups: true,
|
||||
},
|
||||
|
||||
personas: {},
|
||||
@@ -184,8 +187,11 @@ let power_user = {
|
||||
|
||||
persona_description: '',
|
||||
persona_description_position: persona_description_positions.BEFORE_CHAR,
|
||||
persona_show_notifications: true,
|
||||
|
||||
custom_stopping_strings: '',
|
||||
custom_stopping_strings_macro: true,
|
||||
fuzzy_search: false,
|
||||
};
|
||||
|
||||
let themes = [];
|
||||
@@ -255,7 +261,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 +272,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 +540,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 +671,11 @@ function loadPowerUserSettings(settings, data) {
|
||||
power_user.waifuMode = false;
|
||||
}
|
||||
|
||||
if (power_user.chat_width === '') {
|
||||
power_user.chat_width = 50;
|
||||
}
|
||||
|
||||
$('#relaxed_api_urls').prop("checked", power_user.relaxed_api_urls);
|
||||
$('#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 +683,9 @@ 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);
|
||||
$("#custom_stopping_strings_macro").prop("checked", power_user.custom_stopping_strings_macro);
|
||||
$('#fuzzy_search_checkbox').prop("checked", power_user.fuzzy_search);
|
||||
$('#persona_show_notifications').prop("checked", power_user.persona_show_notifications);
|
||||
|
||||
$("#console_log_prompts").prop("checked", power_user.console_log_prompts);
|
||||
$('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown);
|
||||
@@ -837,8 +856,13 @@ function loadInstructMode() {
|
||||
{ id: "instruct_stop_sequence", property: "stop_sequence", isCheckbox: false },
|
||||
{ id: "instruct_names", property: "names", isCheckbox: true },
|
||||
{ id: "instruct_macro", property: "macro", isCheckbox: true },
|
||||
{ id: "instruct_names_force_groups", property: "names_force_groups", isCheckbox: true },
|
||||
];
|
||||
|
||||
if (power_user.instruct.names_force_groups === undefined) {
|
||||
power_user.instruct.names_force_groups = true;
|
||||
}
|
||||
|
||||
controls.forEach(control => {
|
||||
const $element = $(`#${control.id}`);
|
||||
|
||||
@@ -849,7 +873,7 @@ function loadInstructMode() {
|
||||
}
|
||||
|
||||
$element.on('input', function () {
|
||||
power_user.instruct[control.property] = control.isCheckbox ? $(this).prop('checked') : $(this).val();
|
||||
power_user.instruct[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
});
|
||||
@@ -887,8 +911,38 @@ 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 includeNames = isNarrator ? false : power_user.instruct.names;
|
||||
|
||||
if (!isNarrator && power_user.instruct.names_force_groups && (selected_group || forceAvatar)) {
|
||||
includeNames = true;
|
||||
}
|
||||
|
||||
let sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
|
||||
|
||||
if (power_user.instruct.macro) {
|
||||
@@ -916,7 +970,7 @@ export function formatInstructStoryString(story, systemPrompt) {
|
||||
}
|
||||
|
||||
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) {
|
||||
const includeNames = power_user.instruct.names || !!selected_group;
|
||||
const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
|
||||
let sequence = isImpersonate ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
|
||||
|
||||
if (power_user.instruct.macro) {
|
||||
@@ -1151,10 +1205,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 +1227,7 @@ function doNewChat() {
|
||||
|
||||
function doRandomChat() {
|
||||
resetSelectedGroup();
|
||||
setCharacterId(Math.floor(Math.random() * characters.length));
|
||||
setCharacterId(Math.floor(Math.random() * characters.length).toString());
|
||||
setTimeout(() => {
|
||||
reloadCurrentChat();
|
||||
}, 1);
|
||||
@@ -1188,12 +1242,6 @@ async function doMesCut(_, text) {
|
||||
return
|
||||
}
|
||||
|
||||
//reject attempts to delete firstmes
|
||||
if (text === '0') {
|
||||
toastr.error('Cannot delete the First Message')
|
||||
return
|
||||
}
|
||||
|
||||
let mesIDToCut = Number(text).toFixed(0)
|
||||
let mesToCut = $("#chat").find(`.mes[mesid=${mesIDToCut}]`)
|
||||
|
||||
@@ -1947,6 +1995,12 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#relaxed_api_urls").on("input", function () {
|
||||
const value = !!$(this).prop('checked');
|
||||
power_user.relaxed_api_urls = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#spoiler_free_mode').on('input', function () {
|
||||
power_user.spoiler_free_mode = !!$(this).prop('checked');
|
||||
switchSpoilerMode();
|
||||
@@ -1963,6 +2017,21 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#custom_stopping_strings_macro").change(function () {
|
||||
power_user.custom_stopping_strings_macro = !!$(this).prop("checked");
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#fuzzy_search_checkbox').on('input', function () {
|
||||
power_user.fuzzy_search = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#persona_show_notifications').on('input', function () {
|
||||
power_user.persona_show_notifications = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(window).on('focus', function () {
|
||||
browser_has_focus = true;
|
||||
});
|
||||
|
351
public/scripts/preset-manager.js
Normal file
351
public/scripts/preset-manager.js
Normal 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();
|
||||
});
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user