Merge branch 'staging' into inject-filter

47dea8159e256e6083a8da5326fd2596c080112f

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

52 files changed, +3520 -1959Ignore whitespace
.dockerignore+1 -0
@@ -12,3 +12,4 @@ access.log
1212/data
1313/cache
1414.DS_Store
15+/public/scripts/extensions/third-party
.eslintrc.cjs+1 -0
@@ -54,6 +54,7 @@ module.exports = {
5454 },
5555 // These scripts are loaded in HTML; tell ESLint not to complain about them being undefined
5656 globals: {
57+ globalThis: 'readonly',
5758 ePub: 'readonly',
5859 pdfjsLib: 'readonly',
5960 toastr: 'readonly',
.github/readme-de_de.md+1 -1
@@ -8,7 +8,7 @@
88
99<div align="center">
1010
1111[English](readme.md) | German | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
1212
1313[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1414[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-ja_jp.md+1 -1
@@ -5,7 +5,7 @@
55
66<div align="center">
77
88[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | 日本語 | [Русский](readme-ru_ru.md)
99
1010[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1111[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-ru_ru.md+1 -1
@@ -7,7 +7,7 @@
77
88<div align="center">
99
1010[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | Русский
1111
1212[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1313[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-zh_cn.md+1 -1
@@ -5,7 +5,7 @@
55
66<div align="center">
77
88[English](readme.md) | [German](readme-de_de.md) | 中文 | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
99
1010[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1111[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-zh_tw.md+381 -0
@@ -0,0 +1,381 @@
1+> [!IMPORTANT]
2+> 這裡的資訊可能已經過時或不完整,僅供您參考。請使用英文版本以取得最新資訊。
3+
4+<a name="readme-top"></a>
5+
6+![][cover]
7+
8+<div align="center">
9+
10+[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 繁體中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
11+
12+[![GitHub 星標](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
13+[![GitHub 分支](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
14+[![GitHub 問題](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
15+[![GitHub 拉取請求](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
16+
17+</div>
18+
19+---
20+
21+SillyTavern 提供一個統一的前端介面,整合多種大型語言模型的 API(包括:KoboldAI/CPP、Horde、NovelAI、Ooba、Tabby、OpenAI、OpenRouter、Claude、Mistral 等)。同時具備行動裝置友善的佈局、視覺小說模式(Visual Novel Mode)、Automatic1111 與 ComfyUI 的影像生成 API 整合、TTS(語音合成)、世界資訊(Lorebook)、可自訂 UI、自動翻譯功能,以及強大的提示詞(prompt)設定選項和無限的第三方擴充潛力。
22+
23+我們擁有一個 [官方文件網站](https://docs.sillytavern.app/) 可以幫助解答絕大多數的使用問題,並幫助您順利入門。
24+
25+## SillyTavern 是什麼?
26+
27+SillyTavern(簡稱 ST)是一款本地安裝的使用者介面,讓您能與大型語言模型(LLM)、影像生成引擎以及語音合成模型互動的前端。
28+
29+SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發展至今。目前已有超過 100 位貢獻者,並擁有超過兩年的獨立開發歷史。如今,它已成為 AI 愛好者中備受推崇的軟體之一。
30+
31+## 我們的願景
32+
33+1. 我們致力於賦予使用者對 LLM 提示詞的最大控制權與實用性,並認為學習過程中的挑戰是樂趣的一部分
34+2. 我們不提供任何線上或託管服務,也不會程式化追蹤任何使用者數據。
35+3. SillyTavern 是由一群熱衷於 LLM 的開發者社群所打造的熱情專案,並將永遠保持免費與開源。
36+
37+## 分支介紹
38+
39+SillyTavern 採用雙分支開發模式,確保為所有使用者提供流暢的使用體驗。
40+
41+* `release`(穩定版):🌟 **推薦給大部分的使用者使用。** 此分支最為穩定,僅在主要版本發布時更新。適合大多數人,通常每月更新一次。
42+* `staging`(開發版):⚠️ **不建議普通使用者使用。** 此分支包含最新功能,但可能隨時出現問題。適合進階使用者與愛好者,每日多次更新。
43+
44+如果您不熟悉 git CLI 或對分支概念不清楚,請放心對您來說,`release`(穩定版)分支永遠是首選。
45+
46+## 使用 SillyTavern 需要什麼?
47+
48+由於 SillyTavern 僅是一個介面,您需要一個 LLM 後端來提供推理能力。您可以使用 AI Horde 以立即開始聊天。此外,我們支持許多其他本地和雲端 LLM 後端,例如 OpenAI 兼容 API、KoboldAI、Tabby 等。更多支持的 API 資訊,請參閱 [常見問題](https://docs.sillytavern.app/usage/api-connections/)。
49+
50+### 我需要高效能電腦才能運行 SillyTavern 嗎?
51+
52+SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版本的設備都可以執行。若您打算在本地機器上進行 LLM 推理,我們建議使用擁有至少 6GB VRAM 的 3000 系列 NVIDIA 顯示卡。更多詳細資訊,請參考您使用的後端文檔。
53+
54+### 推薦後端(僅為推薦,非官方合作和隸屬關係)
55+
56+* [AI Horde](https://aihorde.net/):使用志願者託管的模型,無需進一步設定
57+* [KoboldCpp](https://github.com/LostRuins/koboldcpp):社群推崇的選擇,可在本地運行 GGUF 模型
58+* [tabbyAPI](https://github.com/theroyallab/tabbyAPI):一個流行且輕量的本地託管 exl2 推理 API
59+* [OpenRouter](https://openrouter.ai):提供多個雲端 LLM 提供商(如 OpenAI、Claude、Meta Llama 等)及熱門社群模型的單一 API
60+
61+## 有任何問題或建議?
62+
63+### 歡迎加入我們的 Discord 伺服器
64+
65+| [![][discord-shield-badge]][discord-link] | [加入我們的 Disocrd 伺服器](https://discord.gg/sillytavern) 以獲得技術支援、分享您喜愛的角色與提示詞。 |
66+| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
67+
68+或直接聯繫開發者:
69+
70+* Discord: cohee, rossascends, wolfsblvt
71+* Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/), [/u/sillylossy](https://www.reddit.com/user/sillylossy/), [u/Wolfsblvt](https://www.reddit.com/user/Wolfsblvt/)
72+* [提交 GitHub 問題](https://github.com/SillyTavern/SillyTavern/issues)
73+
74+### 我喜歡這個專案,我該如何貢獻呢?
75+
76+1. **提交拉取要求(Pull Request)**:想了解如何貢獻,請參閱 [CONTRIBUTING.md](../CONTRIBUTING.md)。
77+2. **提供功能建議與問題報告**:使用本專案所提供的模板提交建議或問題報告。
78+3. **仔細閱讀此 README 文件及相關文檔**:請避免提出重複問題或建議。
79+
80+## 螢幕截圖
81+
82+<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
83+<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
84+
85+## 角色卡
86+
87+SillyTavern 的核心概念是「角色卡」(Character Cards)。角色卡是一組設定 LLM 行為的提示詞,用於 SillyTavern 中進行持續性對話。其功能類似於 ChatGPT 的 GPT 或 Poe 的聊天機器人。角色卡的內容可以是任何形式:抽象場景、針對特定任務設計的助手、知名人物,或者虛構角色。
88+
89+角色卡中唯一必填的項目是名稱欄位。若想與語言模型開始一般對話,您只需創建一個名稱為「Assistant」的新卡片,其餘欄位皆可保持空白。若希望進行更具主題性的對話,則可以提供語言模型背景資訊、行為模式、寫作風格以及特定情境來啟動聊天。
90+
91+如果您僅想進行快速對話而不選擇角色卡片,或想測試 LLM 的連線,則可在打開 SillyTavern 後,於歡迎頁面的輸入欄位中直接輸入您的提示內容。請注意,這類對話是暫時的,不會被永久保存。
92+
93+若想了解如何設定角色卡,可參考預設角色(如 Seraphina)或從「下載擴充功能 & 資源」(Download Extensions & Assets)選單中下載社群製作的角色卡。
94+
95+## 核心功能
96+
97+* 進階文本生成設定:內含許多社群製作的預設設定
98+* 支援世界資訊(World Info):創建豐富的背景故事,或節省角色卡片中的 Token(符記)使用
99+* 群組聊天:多角色聊天室,可讓角色與您或彼此對話
100+* 豐富的 UI 自定義選項:主題顏色、背景圖片、自定義 CSS 等
101+* 使用者設定:讓 AI 更了解您並提升沉浸感
102+* 內建 RAG 支持:可將文檔加入對話,供 AI 參考
103+* 強大的聊天指令子系統:內含 [腳本引擎(Scripting Engine)](https://docs.sillytavern.app/usage/st-script/)
104+
105+## 擴充功能
106+
107+SillyTavern 支持多種擴充功能。
108+
109+* 角色情感表達:使用視覺圖片(立繪)呈現情緒表達
110+* 聊天記錄自動摘要
111+* 自動化介面與聊天翻譯
112+* 穩定擴散(Stable Diffusion)、FLUX 和 DALL-E 的影像生成整合
113+* 語音合成:AI 回應訊息可透過 ElevenLabs、Silero 或系統 TTS 語音合成
114+* 網頁搜尋功能:為提示詞添加真實世界的上下文資訊
115+* 更多擴展:可從「下載擴充功能 & 資源」(Download Extensions & Assets)選單中下載
116+
117+想了解如何使用這些擴充功能,請參考:[官方說明文件](https://docs.sillytavern.app/)
118+
119+# ⌛ 安裝指南
120+
121+> \[!WARNING]
122+>
123+> * 請勿將程式安裝到 Windows 的系統控制資料夾(如 Program Files、System32 等)
124+> * 請勿以管理員權限執行 Start.bat
125+> * 無法在 Windows 7 系統上安裝,因為它無法執行 NodeJS 18.16
126+
127+## 🪟 Windows
128+
129+### 使用 Git 安裝
130+
131+1. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
132+2. 安裝 [Git for Windows](https://gitforwindows.org/)
133+3. 打開 Windows 檔案總管(`Win+E`)
134+4. 創建/使用一個不受 Windows 系統控制或監控的資料夾(例如:C:\MySpecialFolder\)
135+5. 在該資料夾內開啟命令提示字元(Command Prompt):點擊地址欄,輸入 `cmd` 並按下 Enter
136+6. 當命令提示字元黑框彈出時,輸入以下其中一條指令後,按下 Enter:
137+
138+* 安裝 Release(穩定版)分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
139+* 安裝 Staging(開發板)分支:`git clone https://github.com/SillyTavern/SillyTavern -b staging`
140+
141+7. 當程式碼下載完成後,雙擊 `Start.bat`,NodeJS 將自動安裝所需的依賴項
142+8. 本地伺服器啟動後,SillyTavern 將自動在您的瀏覽器中打開
143+
144+### 使用 GitHub Desktop 安裝
145+
146+(此方式僅允許通過 GitHub Desktop 使用 git。如果您也希望在命令列中使用 `git`,則需額外安裝 [Git for Windows](https://gitforwindows.org/))
147+
148+ 1. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
149+ 2. 安裝 [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
150+ 3. 安裝完成後,打開 GitHub Desktop,點擊 `Clone a repository from the internet....` (注意:此步驟 **無需創建 GitHub 帳號**。)
151+ 4. 在彈出選單中,點擊「URL」選項,輸入此網址:`https://github.com/SillyTavern/SillyTavern`,然後點擊「Clone」。您可以更改「Local path」來選擇 SillyTavern 的下載位置
152+ 6. 若想開啟 SillyTavern,需使用 Windows 檔案總管以進入您複製儲存庫的資料夾。預設位置為:`C:\Users\[您的 Windows 使用者名稱]\Documents\GitHub\SillyTavern`
153+ 7. 雙擊 `start.bat` 文件。(請注意:若您的作業系統隱藏了 `.bat` 副檔名,該文件可能顯示為「`Start`」。這就是您需要雙擊運行的文件。)
154+ 8. 雙擊後,將會彈出一個大型黑色的命令提示字元視窗,SillyTavern 會開始安裝其運行所需的文件與依賴
155+ 9. 安裝完成後,若一切正常,命令提示字元視窗應顯示運行中的訊息,且您的瀏覽器會自動打開 SillyTavern 頁籤
156+ 10. 連接到任何 SillyTavern [支援的 APIs](https://docs.sillytavern.app/usage/api-connections/) 並開始聊天吧!
157+
158+## 🐧 Linux & 🍎 MacOS
159+
160+對於 MacOS 和 Linux 系統,所有操作都將在終端機(Terminal)中完成。
161+
162+1. 安裝 git 和 NodeJS(具體方法因操作系統而異)
163+2. 複製儲存庫(Clone the repo):
164+
165+* 安裝 Release(穩定版)分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
166+* 安裝 Staging(開發板)分支:`git clone https://github.com/SillyTavern/SillyTavern -b staging`
167+
168+3. 使用命令 `cd SillyTavern` 以進入安裝資料夾
169+4. 使用以下其中一條命令,以執行 `start.sh` 腳本:
170+
171+* `./start.sh`
172+* `bash start.sh`
173+
174+## ⚡ 使用 SillyTavern Launcher 安裝
175+
176+SillyTavern Launcher 是一個安裝嚮導,協助您設定多種選項,包括安裝本地推理(inference)的後端。
177+
178+### 對於 Windows 使用者
179+
180+1. 在鍵盤上按下 **`WINDOWS + R`** 打開「執行」對話框,然後輸入以下指令以安裝 git:
181+
182+```shell
183+cmd /c winget install -e --id Git.Git
184+```
185+
186+2. 在鍵盤上按下 **`WINDOWS + E`** 打開檔案總管,導航至您想要安裝 Launcher 的資料夾。在目標資料夾的地址欄輸入 `cmd` 並按下 Enter。接著執行以下命令:
187+
188+```shell
189+git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
190+```
191+
192+### 對於 Linux 使用者
193+
194+1. 打開您喜歡的終端機(Terminal),安裝 git
195+2. 使用以下指令以複製 Sillytavern-Launcher:
196+
197+```shell
198+git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
199+```
200+
201+3. 執行安裝腳本(installer.sh):
202+
203+```shell
204+chmod +x install.sh && ./install.sh
205+```
206+
207+4. 安裝完成後,執行啟動腳本(launcher.sh):
208+
209+```shell
210+chmod +x launcher.sh && ./launcher.sh
211+```
212+
213+### 對於 Mac 使用者
214+
215+1. 打開終端機(Terminal),並使用以下指令安裝 Homebrew:
216+
217+```shell
218+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
219+```
220+
221+2. 使用 Homebrew 以安裝 git:
222+
223+```shell
224+brew install git
225+```
226+
227+3. 使用以下指令以複製 Sillytavern-Launcher:
228+
229+```shell
230+git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
231+```
232+
233+4. 執行安裝腳本(installer.sh):
234+
235+```shell
236+chmod +x install.sh && ./install.sh
237+```
238+
239+5. 安裝完成後,執行啟動腳本(launcher.sh):
240+
241+```shell
242+chmod +x launcher.sh && ./launcher.sh
243+```
244+
245+## 🐋 使用 Docker 安裝
246+
247+以下指南已假設您安裝 Docker,能夠訪問命令列進行容器安裝,並熟悉 Docker 的基本使用。
248+
249+### 自行構建映像
250+
251+我們提供了一份完整的 [SillyTavern Docker 使用指南](http://docs.sillytavern.app/installation/docker/)。該指南涵蓋了 Windows、macOS 和 Linux 的安裝過程。若您希望自行構建映像,建議先閱讀該文檔。
252+
253+### 使用 GitHub 容器註冊表(最簡易的方式)
254+
255+您需要設定兩個必要的目錄映射(directory mappings)和一個端口映射(port mapping)來使 SillyTavern 正常運行。在執行指令時,請將以下佔位符替換為您的實際配置:
256+
257+#### 容器變數
258+
259+##### 目錄映射(Volume Mappings)
260+
261+* [config]:用於存放 SillyTavern 設定文件的本地資料夾
262+* [data]:用於存放 SillyTavern 使用者數據(包括角色)的本地資料夾
263+* [plugins](可選):用於存放 SillyTavern 擴充功能的本地資料夾
264+
265+##### 端口映射(Port Mappings)
266+
267+* [PublicPort]:對外流量的訪問端口。這是必需的,因為您將從虛擬機容器外部訪問實例。除非實施了額外的安全服務,否則請勿將此端口暴露於網路
268+
269+##### 其他設定(Additional Settings)
270+
271+* [DockerNet]:容器應連接的 Docker 網路。如果您不熟悉此概念,請參閱 [Docker 官方說明文件](https://docs.docker.com/reference/cli/docker/network/)
272+* [version]:在 GitHub 頁面的右側,您可以找到「Packages」。選擇「sillytavern」包,然後查看映像版本。「latest」標籤會使您保持與當前版本同步。您也可以選擇「staging」或「release」標籤,但這可能不適用於依賴擴充功能的使用者,因為擴充功能可能需要時間進行更新
273+
274+#### 安裝命令
275+
276+1. 打開命令列(Command Line)
277+2. 執行以下指令:
278+
279+`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
280+
281+> 請注意:默認的監聽端口為 8000。如果您在設定文件中更改了此端口,請務必使用適當的端口號
282+
283+## 📱 於 Android 系統中使用 Termux 安裝
284+
285+> \[!NOTE]
286+> **雖然您可以在 Android 設備上使用 Termux 直接運行 SillyTavern,但這不在我們的官方支持範圍內。**
287+>
288+> **請參閱 ArroganceComplex#2659 所提供的指南:**
289+>
290+> * <https://rentry.org/STAI-Termux>
291+
292+**不支援:Android ARM LEtime-web。** 32 位 Android 系統需要額外的依賴項,這無法通過 npm 安裝。請使用以下命令安裝:`pkg install esbuild`。完成後,請按照普通的安裝步驟進行操作
293+
294+## API 金鑰管理
295+
296+SillyTavern 將您的 API 金鑰(Keys)保存在使用者數據目錄中的 `secrets.json` 文件內(默認路徑為`/data/default-user/secrets.json`)
297+
298+默認情況下,API 金鑰在您保存並重新載入頁面後,將不會自介面中顯示
299+
300+如需啟用查看金鑰功能:
301+
302+1. 在 `config.yaml` 文件中,將 `allowKeysExposure` 的「值」設為 `true`
303+2. 重新啟動 SillyTavern 伺服器
304+3. 點擊 API 連線頁面右下角的「查看隱藏的 API 金鑰(View hidden API keys)」超連結
305+
306+## 命令列參數(Command-line Arguments)
307+
308+您可以在啟動 SillyTavern 伺服器時傳遞命令列參數,以覆蓋 `config.yaml` 文件中的某些設定。
309+
310+### 範例
311+
312+```shell
313+node server.js --port 8000 --listen false
314+# or
315+npm run start -- --port 8000 --listen false
316+# or(僅適用於 Windows)
317+Start.bat --port 8000 --listen false
318+```
319+
320+### Supported arguments
321+
322+| Option | Description | Type |
323+|-------------------------|------------------------------------------------------------------------------------------------------|----------|
324+| `--version` | 顯示版本序號 | boolean |
325+| `--enableIPv6` | 啟用 IPv6 | boolean |
326+| `--enableIPv4` | 啟用 IPv4 | boolean |
327+| `--port` | 設定 SillyTavern 運行的端口。若未提供,則預設使用 `config.yaml` 中的 'port' | number
328+| `--dnsPreferIPv6` | 偏好使用 IPv6 解析 DNS。未提供則默認使用 `config.yaml` 中的 'preferIPv6' | boolean |
329+| `--autorun` | 自動在瀏覽器中啟動 SillyTavern。未提供則默認使用 `config.yaml` 中的 'autorun' | boolean |
330+| `--autorunHostname` | 自動啟動時的主機名稱,通常建議保持為 'auto' | string |
331+| `--autorunPortOverride` | 覆蓋自動啟動的端口設定 | string |
332+| `--listen` | SillyTavern 是否可監聽所有網路接口。若未提供,則默認使用 `config.yaml` 中的 'listen' | boolean |
333+| `--corsProxy` | 啟用 CORS 代理。若未提供,則默認使用 `config.yaml` 中的 'enableCorsProxy' | boolean |
334+| `--disableCsrf` | 停用 CSRF 保護 | boolean |
335+| `--ssl` | 啟用 SSL | boolean |
336+| `--certPath` | 設定您證書文件的路徑 | string |
337+| `--keyPath` | 設定您私人金鑰文件的路徑 | string |
338+| `--whitelist` | 啟用白名單模式 | boolean |
339+| `--dataRoot` | 設定數據儲存的根目錄 | string |
340+| `--avoidLocalhost` | 在自動模式下避免使用 'localhost' | boolean |
341+| `--basicAuthMode` | 啟用基本身份驗證模式 | boolean |
342+| `--requestProxyEnabled` | 啟用代理以處理外部請求 | boolean |
343+| `--requestProxyUrl` | 設定請求代理的 URL(支持 HTTP 或 SOCKS 協議) | string |
344+| `--requestProxyBypass` | 請求代理的例外主機清單(主機列表需以空格分隔) | array |
345+
346+## 遠端連線
347+
348+遠端連線功能最常用於希望在手機上使用 SillyTavern 的使用者。此時伺服器將由同一 Wi-Fi 網路上的 PC 運行。不過,您也可以設定來自其他網路的遠端連線。
349+
350+詳細設定指南請參閱 [官方說明文件](https://docs.sillytavern.app/usage/remoteconnections/)。
351+
352+您還可以選擇設定 SillyTavern 的使用者檔案,並開啟密碼保護(可選):[使用者設定指南](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users)。
353+
354+## 遇到任何效能問題?
355+
356+1. 在「使用者設定」選單(設定介面主題)中,禁用模糊效果(Blur Effect),並開啟「減少動畫效果」(Reduced Motion)
357+2. 若使用響應串流傳輸,請將串流的 FPS 設定為較低的值(建議設定為 10-15 FPS)
358+3. 確保瀏覽器已啟用 GPU 加速以進行渲染
359+
360+## 授權與致謝
361+
362+**本程式(SillyTavern)的發布是基於其可能對使用者有所幫助的期許,但不提供任何形式的保證;包括但不限於對可銷售性(marketability)或特定用途適用性的隱含保證。如需更多詳情,請參閱 GNU Affero 通用公共許可證。**
363+
364+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.
365+
366+* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 由 Humi 提供:MIT 許可
367+* 經授權使用部分來自 CncAnon 的 TavernAITurbo 模組
368+* 視覺小說模式(Visual Novel Mode)的靈感,來源於 PepperTaco 的貢獻(<https://github.com/peppertaco/Tavern/>)
369+* Noto Sans 字體由 Google 提供(OFL 許可)
370+* 主題圖示由 Font Awesome <https://fontawesome.com> 提供(圖示:CC BY 4.0,字體:SIL OFL 1.1,代碼:MIT 許可)
371+* 預設資源來源於 @OtisAlejandro(包含角色 Seraphina 與知識書)與 @kallmeflocc(SillyTavern 官方 Discord 伺服器成員突破 10K 的慶祝背景)
372+* Docker 安裝指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 編寫
373+
374+## 主要貢獻者
375+
376+[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
377+
378+<!-- LINK GROUP -->
379+[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
380+[discord-link]: https://discord.gg/sillytavern
381+[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
.github/readme.md+1 -1
@@ -4,7 +4,7 @@
44
55<div align="center">
66
77English | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
88
99[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1010[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.gitignore+2 -0
@@ -50,3 +50,5 @@ public/css/user.css
5050/default/scaffold
5151public/scripts/extensions/third-party
5252/certs
53+.aider*
54+.env
.npmignore+1 -0
@@ -11,3 +11,4 @@ access.log
1111.github
1212.vscode
1313.git
14+/public/scripts/extensions/third-party
Dockerfile+2 -4
@@ -6,9 +6,6 @@ ARG APP_HOME=/home/node/app
66# Install system dependencies
77RUN apk add gcompat tini git
88
9-# Ensure proper handling of kernel signals
10-ENTRYPOINT [ "tini", "--" ]
11-
129# Create app directory
1310WORKDIR ${APP_HOME}
1411
@@ -42,4 +39,5 @@ RUN \
4239
4340EXPOSE 8000
4441
45-CMD [ "./docker-entrypoint.sh" ]
42+# Ensure proper handling of kernel signals
43+ENTRYPOINT ["tini", "--", "./docker-entrypoint.sh"]
default/config.yaml+2 -0
@@ -98,6 +98,8 @@ skipContentCheck: false
9898disableChatBackup: false
9999# Number of backups to keep for each chat and settings file
100100numberOfBackups: 50
101+# Maximum number of chat backups to keep per user (starting from the most recent). Set to -1 to keep all backups.
102+maxTotalChatBackups: -1
101103# Interval in milliseconds to throttle chat backups per user
102104chatBackupThrottleInterval: 10000
103105# Allowed hosts for card downloads
docker/docker-entrypoint.sh+1 -1
@@ -6,4 +6,4 @@ if [ ! -e "config/config.yaml" ]; then
66fi
77
88# Start the server
99exec node server.js --listen "$@"
package-lock.json+2 -2
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.12.89",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.12.89",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
package.json+1 -1
@@ -84,7 +84,7 @@
8484 "type": "git",
8585 "url": "https://github.com/SillyTavern/SillyTavern.git"
8686 },
8787 "version": "1.12.89",
8888 "scripts": {
8989 "start": "node server.js",
9090 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
public/css/extensions-panel.css+47 -6
@@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] {
6565}
6666
6767.extensions_info .extension_enabled {
6868 colorfont-weight: greenbold;
6969}
7070
7171.extensions_info .extension_disabled {
@@ -76,13 +76,44 @@ label[for="extensions_autoconnect"] {
7676 color: gray;
7777}
7878
79-input.extension_missing[type="checkbox"] {
79+.extensions_info .extension_modules {
8080 opacityfont-size: 0.58em;
81+ font-weight: normal;
8182}
8283
8384#extensions_list.extensions_info .disabledextension_block {
8485 text-decorationdisplay: line-throughflex;
8586 colorflex-wrap: lightgraynowrap;
87+ padding: 5px;
88+ margin-bottom: 5px;
89+ border: 1px solid var(--SmartThemeBorderColor);
90+ border-radius: 10px;
91+ align-items: baseline;
92+ justify-content: space-between;
93+ gap: 5px;
94+}
95+
96+.extensions_info .extension_name {
97+ font-size: 1.05em;
98+}
99+
100+.extensions_info .extension_version {
101+ opacity: 0.8;
102+ font-size: 0.8em;
103+ font-weight: normal;
104+ margin-left: 2px;
105+}
106+
107+.extensions_info .extension_block a {
108+ color: var(--SmartThemeBodyColor);
109+}
110+
111+.extensions_info .extension_name.update_available {
112+ color: limegreen;
113+}
114+
115+input.extension_missing[type="checkbox"] {
116+ opacity: 0.5;
86117}
87118
88119.update-button {
@@ -105,3 +136,13 @@ input.extension_missing[type="checkbox"] {
105136#extensionsMenu>div.extension_container:empty {
106137 display: none;
107138}
139+
140+.extensions_info .extension_text_block {
141+ white-space: nowrap;
142+ overflow: hidden;
143+ text-overflow: ellipsis;
144+}
145+
146+.extensions_info .extension_actions {
147+ flex-wrap: nowrap;
148+}
public/global.d.ts+2 -1
@@ -1,4 +1,5 @@
11import libs from './lib';
2+import getContext from './scripts/st-context';
23
34// Global namespace modules
45declare var ai;
@@ -6,7 +7,7 @@ declare var pdfjsLib;
67declare var ePub;
78
89declare var SillyTavern: {
910 getContext(): anytypeof getContext;
1011 llm: any;
1112 libs: typeof libs;
1213};
public/index.html+39 -23
@@ -1774,6 +1774,35 @@
17741774 <span data-i18n="Load default order">Load default order</span>
17751775 </div>
17761776 </div>
1777+ <div id="sampler_priority_block_aphrodite" data-tg-type="aphrodite" class="range-block flexFlowColumn wide100p">
1778+ <hr class="wide100p">
1779+ <h4 class="range-block-title justifyCenter">
1780+ <span data-i18n="Sampler Order">Sampler Order</span>
1781+ <div class="margin5 fa-solid fa-circle-info opacity50p" title="Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here." data-i18n="[title]Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here."></div>
1782+ </h4>
1783+ <div class="toggle-description widthUnset" data-i18n="Aphrodite only. Determines the order of samplers.">
1784+ Aphrodite only. Determines the order of samplers.
1785+ </div>
1786+ <div id="sampler_priority_container_aphrodite" class="prompt_order">
1787+ <div data-name="dry" draggable="true"><span>DRY</span><small></small></div>
1788+ <div data-name="penalties" draggable="true"><span>Penalties</span><small></small></div>
1789+ <div data-name="no_repeat_ngram" draggable="true"><span>No Repeat Ngram</span><small></small></div>
1790+ <div data-name="temperature" draggable="true"><span>Dynatemp & Temperature</span><small></small></div>
1791+ <div data-name="top_nsigma" draggable="true"><span>Top Nsigma</span><small></small></div>
1792+ <div data-name="top_p_top_k" draggable="true"><span>Top P & Top K</span><small></small></div>
1793+ <div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>
1794+ <div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>
1795+ <div data-name="tfs" draggable="true"><span>Tail-Free Sampling</span><small></small></div>
1796+ <div data-name="eta_cutoff" draggable="true"><span>Eta Cutoff</span><small></small></div>
1797+ <div data-name="epsilon_cutoff" draggable="true"><span>Epsilon Cutoff</span><small></small></div>
1798+ <div data-name="typical_p" draggable="true"><span>Typical P</span><small></small></div>
1799+ <div data-name="quadratic" draggable="true"><span>Cubic and Quadratic Sampling</span><small></small></div>
1800+ <div data-name="xtc" draggable="true"><span>XTC</span><small></small></div>
1801+ </div>
1802+ <div id="aphrodite_default_order" class="menu_button menu_button_icon">
1803+ <span data-i18n="Load default order">Load default order</span>
1804+ </div>
1805+ </div>
17771806 </div>
17781807 </div><!-- end of textgen settings-->
17791808 <div id="openai_settings">
@@ -2985,8 +3014,10 @@
29853014 <option value="chat-bison-001">PaLM 2 Chat (Legacy)</option>
29863015 </optgroup>
29873016 <optgroup label="Subversions">
29883017 <option value="gemini-exp2.0-1121flash-exp">Gemini Experimental2.0 2024-11-21Flash Experiment</option>
29893018 <option value="gemini-exp-1114">Gemini Experimental 2024-11-14</option>
3019+ <option value="gemini-exp-1121">Gemini Experimental 2024-11-21</option>
3020+ <option value="gemini-exp-1206">Gemini Experimental 2024-12-06</option>
29903021 <option value="gemini-1.5-pro-exp-0801">Gemini 1.5 Pro Experiment 2024-08-01</option>
29913022 <option value="gemini-1.5-pro-exp-0827">Gemini 1.5 Pro Experiment 2024-08-27</option>
29923023 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
@@ -3070,6 +3101,9 @@
30703101 </div>
30713102 <h4 data-i18n="Groq Model">Groq Model</h4>
30723103 <select id="model_groq_select">
3104+ <optgroup label="Llama 3.3">
3105+ <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
3106+ </optgroup>
30733107 <optgroup label="Llama 3.2">
30743108 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
30753109 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
@@ -4183,7 +4217,7 @@
41834217 </label>
41844218 <label for="show_swipe_num_all_messages" class="checkbox_label" title="Display swipe numbers for all messages, not just the last." data-i18n="[title]Display swipe numbers for all messages, not just the last.">
41854219 <input id="show_swipe_num_all_messages" type="checkbox" />
41864220 <small data-i18n="Swipe # for All Messages">Swipe # for All Messages</small><i class="fa-solid fa-mobile-screen-button"></i>
41874221 </label>
41884222 <label for="hotswapEnabled" class="checkbox_label" title="In the Character Management panel, show quick selection buttons for favorited characters." data-i18n="[title]In the Character Management panel, show quick selection buttons for favorited characters">
41894223 <input id="hotswapEnabled" type="checkbox" />
@@ -4813,6 +4847,8 @@
48134847 </div>
48144848 <div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages">
48154849 </div>
4850+ <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook">
4851+ </div>
48164852 </div>
48174853 <div>
48184854 <h4 data-i18n="Persona Description">Persona Description</h4>
@@ -4929,7 +4965,7 @@
49294965 <input type="hidden" id="fav_checkbox" name="fav" />
49304966 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
49314967 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>
49324968 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div>
49334969 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
49344970 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
49354971 <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> -->
@@ -5539,26 +5575,6 @@
55395575 </div>
55405576 </div>
55415577 <!-- templates for JS to reuse when needed -->
5542- <div id="chat_world_template" class="template_element">
5543- <div class="chat_world range-block flexFlowColumn flex-container">
5544- <div class="range-block-title">
5545- <h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
5546- <span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
5547- </h4>
5548- </div>
5549- <div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
5550- <span data-i18n="chat_world_template_txt">
5551- A selected World Info will be bound to this chat. When generating an AI reply,
5552- it will be combined with the entries from global and character lorebooks.
5553- </span>
5554- </div>
5555- <div class="range-block-range wide100p">
5556- <select class="chat_world_info_selector wide100p">
5557- <option value="">--- None ---</option>
5558- </select>
5559- </div>
5560- </div>
5561- </div>
55625578 <div id="character_world_template" class="template_element">
55635579 <div class="character_world range-block flexFlowColumn flex-container">
55645580 <div class="range-block-title">
public/jsconfig.json+2 -1
@@ -5,7 +5,8 @@
55 "module": "ESNext",
66 "moduleResolution": "node",
77 "allowUmdGlobalAccess": true,
88 "allowSyntheticDefaultImports": true,
9+ "strictBindCallApply": true
910 },
1011 "exclude": [
1112 "**/node_modules/**",
public/locales/zh-tw.json+698 -353
@@ -2,9 +2,9 @@
22 "Favorite": "我的最愛",
33 "Tag": "標籤",
44 "Duplicate": "複製",
55 "Persona": "玩家角色人物使用者角色",
66 "Delete": "刪除",
77 "AI Response Configuration": "設定 AI 回應配置回應",
88 "AI Configuration panel will stay open": "上鎖 = AI 設定面板將保持開啟",
99 "clickslidertips": "點選滑桿下的數字可手動輸入。",
1010 "MAD LAB MODE ON": "瘋狂實驗室模式",
@@ -27,13 +27,13 @@
2727 "Instruct": "指示",
2828 "Prose Augmenter": "散文增強器",
2929 "Text Adventure": "文字冒險",
3030 "response legth(tokens)": "回應長度(符記數)(符元數)",
3131 "Streaming": "串流",
3232 "Streaming_desc": "生成時逐位顯示回應。當此功能關閉時,回應將在完成後一次顯示。",
3333 "context size(tokens)": "上下文大小(符記數)上下文長度(符元數)",
3434 "unlocked": "解鎖",
3535 "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過8192個符記的上下文大小時啟用此功能僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能",
3636 "Max prompt cost:": "最多提示詞費用最大提示詞費用",
3737 "Display the response bit by bit as it is generated.": "生成時逐位顯示回應。",
3838 "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在完成後一次性顯示。",
3939 "Temperature": "溫度",
@@ -50,10 +50,10 @@
5050 "Medium": "中等",
5151 "Aggressive": "積極",
5252 "Very aggressive": "非常積極",
5353 "Unlocked Context Size": "解鎖的上下文大小解鎖的上下文長度",
5454 "Unrestricted maximum value for the context slider": "上下文滑桿的無限制最大值",
5555 "Context Size (tokens)": "上下文大小(符記數)上下文長度(符元數)",
5656 "Max Response Length (tokens)": "最大回應長度(符記數)(符元數)",
5757 "Multiple swipes per generation": "每次生成多次滑動",
5858 "Enable OpenAI completion streaming": "啟用 OpenAI 補充串流",
5959 "Frequency Penalty": "頻率懲罰",
@@ -80,7 +80,7 @@
8080 "Scenario Format Template": "情境格式範本",
8181 "Personality Format Template": "個性格式範本",
8282 "Group Nudge Prompt Template": "群組推動提示詞範本",
8383 "Sent at the end of the group chat history to force reply from a specific character.": "在群組聊天歷史結束時發送以強制特定角色人物回覆在群組聊天歷史結束時發送以強制特定角色回覆",
8484 "New Chat": "新聊天",
8585 "Restore new chat prompt": "還原新聊天的提示詞",
8686 "Set at the beginning of the chat history to indicate that a new chat is about to start.": "設定在聊天歷史的開頭以表明即將開始新的聊天",
@@ -91,18 +91,18 @@
9191 "Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "設定在對話範例的開頭以表明即將開始新的範例聊天",
9292 "Continue nudge": "繼續輔助提示詞",
9393 "Set at the end of the chat history when the continue button is pressed.": "按下繼續按鈕時設定在聊天歷史的末尾",
9494 "Replace empty message": "取代空訊息取代空白訊息",
9595 "Send this text instead of nothing when the text box is empty.": "當文字方塊為空時,發送此字串而不是空白。",
9696 "Seed": "種子",
9797 "Set to get deterministic results. Use -1 for random seed.": "設置以獲取確定性結果設定以獲取確定性結果。使用 -1 作為隨機種子",
9898 "Temperature controls the randomness in token selection": "溫度控制符記選擇中的隨機性溫度控制符元選擇中的隨機性",
9999 "Top_K_desc": "Top K 設定可以選擇的最高符記數量設定可以選擇的最高符元數量。\n例如,Top K 為 20,這意味著只保留排名前 20 的符記的符元(無論它們的機率是多樣還是有限的)。\n設定為 0 以停用。",
100100 "Top_P_desc": "Top P(又名核心取樣)會將所有頂級符記加總會將所有頂級符元加總,直到達到目標百分比。\n例如,如果前兩個符記都是如果前兩個符元都是 25%,而 Top P 設為 0.5,那麼只有前兩個符記會被考慮那麼只有前兩個符元會被考慮。\n設定為 1.0 以停用。",
101101 "Typical P": "Typical P",
102102 "Typical_P_desc": "Typical P 取樣根據符記偏離集合平均熵的程度進行優先排序取樣根據符元偏離集合平均熵的程度進行優先排序。\n它會保留累積機率接近預設閾值(例如0.5)的符記的符元,強調那些具有平均信息量的符記強調那些具有平均信息量的符元。\n設定為 1.0 以停用。",
103103 "Min_P_desc": "Min P 設定基本最小機率。\n這個值會根據最高符記的機率進行調整n這個值會根據最高符元的機率進行調整。例如,如果最高符記機率為如果最高符元機率為 80%,而 Min P 設為 0.1 ,那麼只有機率高於 8% 的符記會被考慮的符元會被考慮。\n設定為 0 以停用。",
104104 "Top_A_desc": "Top A 根據最高符記機率的平方設定符記選擇的門檻根據最高符元機率的平方設定符元選擇的門檻。\n例如,如果 Top A 值為 0.2,而最高符記機率為而最高符元機率為 50%,那麼低於 5%(0.2 * 0.5^2) 的符記機率就會被排除的符元機率就會被排除。\n設定為 0 以停用。",
105105 "Tail_Free_Sampling_desc": "無尾取樣(Tail-Free Sampling, TFS)會透過分析符記機率的變化率會透過分析符元機率的變化率(使用導數)來尋找分佈中的低機率符記尾部來尋找分佈中的低機率符元尾部。\n它會根據標準化的二階導數,保留直到某個閾值(例如0.3)的符記的符元。\n數值越接近 0 ,表示會棄去越多符記表示會棄去越多符元。設定為 1.0 以停用。",
106106 "rep.pen range": "重複懲罰範圍",
107107 "Mirostat": "Mirostat",
108108 "Mode": "模式",
@@ -111,8 +111,8 @@
111111 "Mirostat_Tau_desc": "這個設定控制了 Mirostat 輸出的可變性。",
112112 "Eta": "Eta",
113113 "Mirostat_Eta_desc": "這個設定控制 Mirostat 的學習率。",
114114 "Ban EOS Token": "禁止 EOS 符記符元",
115115 "Ban_EOS_Token_desc": "對於 KoboldCpp (以及可能也適用於 KoboldAI 的其他符記)的其他符元),禁止使用序列結束 (End-of-Sequence, EOS) 符記符元。這個設定對於故事創作很有幫助,但不應該在聊天和指令模式中使用。",
116116 "GBNF Grammar": "GBNF 語法",
117117 "Type in the desired custom grammar": "輸入所需的自定義語法",
118118 "Samplers Order": "取樣器順序",
@@ -121,15 +121,15 @@
121121 "Load koboldcpp order": "載入 koboldcpp 順序",
122122 "Preamble": "前言",
123123 "Use style tags to modify the writing style of the output.": "使用樣式標籤修改輸出的寫作樣式。",
124124 "Banned Tokens": "禁止的符記禁止的符元",
125125 "Sequences you don't want to appear in the output. One per line.": "您不希望在輸出中出現的序列。每行一個,使用文字或符記使用文字或符元 ID。",
126126 "Logit Bias": "Logit 偏差",
127127 "Add": "新增",
128128 "Helps to ban or reenforce the usage of certain words": "有助於禁止或強化某些符記的使用有助於禁止或強化某些符元的使用",
129129 "CFG Scale": "CFG 比例",
130130 "Negative Prompt": "負面提示詞",
131131 "Add text here that would make the AI generate things you don't want in your outputs.": "在這裡新增文字,使 AI 生成您不希望在輸出中出現的內容。",
132132 "Used if CFG Scale is unset globally, per chat or character": "如果CFG Scale未在全域、每個聊天或角色人物中設定每個聊天或角色中設定,則使用",
133133 "Mirostat Tau": "Tau",
134134 "Mirostat LR": "Mirostat 學習率",
135135 "Min Length": "最小長度",
@@ -138,23 +138,23 @@
138138 "Top A Sampling": "Top A 取樣",
139139 "CFG": "CFG",
140140 "Neutralize Samplers": "中和取樣器",
141141 "Set all samplers to their neutral/disabled state.": "將所有取樣器設定為中性/停用狀態。",
142142 "Sampler Select": "選擇取樣器",
143143 "Customize displayed samplers or add custom samplers.": "自訂顯示的取樣器或新增自訂取樣器。",
144144 "Epsilon Cutoff": "Epsilon 截斷",
145145 "Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "Epsilon 截斷設定排除符記的機率下限截斷設定排除符元的機率下限",
146146 "Eta Cutoff": "Eta 截斷",
147147 "Eta_Cutoff_desc": "Eta 截斷是特殊 Eta 取樣技術的主要參數。\n單位為 1e-4;合理值為 3。\n設為 0 以停用。\n詳情請參見 Hewitt 等人(等人於 2022)撰寫的論文 年撰寫的論文《Truncation Sampling as Language Model Desmoothing》。",
148148 "rep.pen decay": "重複懲罰衰減",
149149 "Encoder Rep. Pen.": "編碼器重複懲罰",
150150 "No Repeat Ngram Size": "無重複Ngram大小無重複 Ngram 大小",
151151 "Skew": "Skew",
152152 "Max Tokens Second": "最大符記/最大符元/秒",
153153 "Smooth Sampling": "平滑取樣",
154154 "Smooth_Sampling_desc": "允許您使用二次/三次變換來調整分佈。較低的平滑因子值將更具創造性,通常在 0.2-0.3 之間是最佳點(假設曲線=1)。較高的平滑曲線值會使曲線更陡峭,這將更加激烈地懲罰低概率選擇。1.0 的曲線值相當於僅使用平滑因子。",
155155 "Smoothing Factor": "平滑因子",
156156 "Smoothing Curve": "平滑曲線",
157157 "DRY_Repetition_Penalty_desc": "DRY 會懲罰那些將輸入的結尾擴充為已在先前輸入中出現過序列的符記會懲罰那些將輸入的結尾擴充為已在先前輸入中出現過序列的符元。將乘法器設為 0 以停用。",
158158 "DRY Repetition Penalty": "DRY 重複懲罰",
159159 "DRY_Multiplier_desc": "將值設為大於 0 來啟用 DRY。控制對最短受懲罰序列的懲罰幅度。",
160160 "Multiplier": "乘法器",
@@ -163,42 +163,42 @@
163163 "DRY_Allowed_Length_desc": "可以重複而不受懲罰的最長序列長度。",
164164 "Allowed Length": "允許長度",
165165 "Penalty Range": "懲罰範圍",
166166 "DRY_Sequence_Breakers_desc": "序列停止繼續配對的符記序列停止繼續配對的符元,使用引號包裹字串並以逗號分隔清單。",
167167 "Sequence Breakers": "序列中斷器",
168168 "JSON-serialized array of strings.": "序列化JSON的字串陣列序列化 JSON 的字串陣列。",
169169 "Dynamic Temperature": "動態溫度",
170170 "Scale Temperature dynamically per token, based on the variation of probabilities": "根據機率變化,動態調整每個符記的溫度動態調整每個符元的溫度",
171171 "Minimum Temp": "最低溫度",
172172 "Maximum Temp": "最高溫度",
173173 "Exponent": "指數",
174174 "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1僅適用於llama1 僅適用於 llama.cpp)",
175175 "Mirostat_desc": "Mirostat 是輸出困惑度的恆溫器。\nMirostat 會將輸出的困惑度與輸入的困惑度相配對,\n從而避免了重複陷阱(在這個陷阱中,隨著自回歸推理產生字串,輸出的困惑度趨於零)和混亂陷阱(困惑度發散)。\n有關詳細資訊,請參閱 Basu 等人於 2020 年發表的論文《A Neural Text Decoding Algorithm that Directly Controls Perplexity》。\n模式選擇 Mirostat 版本。0=停用,1=Mirostat 1.0(僅限 llama.cpp),2=Mirostat 2.0。",
176176 "Mirostat Mode": "Mode",
177177 "Variability parameter for Mirostat outputs": "Mirostat 輸出的變異性參數",
178178 "Mirostat Eta": "Eta",
179179 "Learning rate of Mirostat": "Mirostat 的學習率",
180180 "Beam search": "Beam search(波束搜尋",
181181 "Helpful tip coming soon.": "更多有用提示訊息即將推出。",
182182 "Number of Beams": "波束數量",
183183 "Length Penalty": "長度懲罰",
184184 "Early Stopping": "提前停止",
185185 "Contrastive search": "對比搜尋",
186186 "Contrastive_search_txt": "一種取樣器,通過利用大多數 LLM 的表示空間的等向性,鼓勵多樣性的同時保持一致性。詳情請參閱 Su 等人於2022年發表的論文等人於 2022 年發表的論文《A Contrastive Framework for Neural Text Generation》。",
187187 "Penalty Alpha": "懲罰 Alpha",
188188 "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "對比搜尋正則化項的強度。設定為0以停用CS設定為 0 以停用 CS",
189189 "Do Sample": "進行取樣",
190190 "Add BOS Token": "新增 BOS 符記符元",
191191 "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "在提示詞的開頭新增 bos_token。停用此功能可以使回應更具創造性",
192192 "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止 eos_token。這迫使模型不會提前結束生成",
193193 "Ignore EOS Token": "忽略 EOS 符記符元",
194194 "Ignore the EOS Token even if it generates.": "即使生成也忽略 EOS 符記符元",
195195 "Skip Special Tokens": "跳過特殊符記跳過特殊符元",
196196 "Temperature Last": "最後的溫度",
197197 "Temperature_Last_desc": "使用最後應用溫度取樣器。這幾乎總是明智的做法。\n啟用時:首先取樣一組合理的符記首先取樣一組合理的符元,然後應用溫度來調整它們的相對機率(技術上講,是 logits)。\n停用時:首先應用溫度調整所有符記的相對機率首先應用溫度調整所有符元的相對機率然後從中取樣合理的符記然後從中取樣合理的符元。\n停用「最後應用溫度取樣」會增加分佈尾部的概率,這傾向於放大獲得不連貫回應的機會。",
198198 "Speculative Ngram": "推測性 Ngram",
199199 "Use a different speculative decoding method without a draft model": "使用不含草稿模型的不同推測性解碼方法。",
200200 "Spaces Between Special Tokens": "特殊符記之間的空格特殊符元之間的空格",
201201 "LLaMA / Mistral / Yi models only": "僅限 LLaMA / Mistral / Yi 模型",
202202 "Example: some text [42, 69, 1337]": "範例:\n一些文字 [42, 69, 1337]",
203203 "Classifier Free Guidance. More helpful tip coming soon": "無分類器導引。更多有用提示訊息即將推出",
204204 "Scale": "比例",
@@ -211,16 +211,16 @@
211211 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",
212212 "Sampler Priority": "取樣器優先順序",
213213 "Ooba only. Determines the order of samplers.": "僅適用於 Ooba。決定取樣器的順序。",
214214 "Character Names Behavior": "角色人物名稱行為角色名稱行為",
215215 "Helps the model to associate messages with characters.": "幫助模型將訊息與角色人物關聯起來幫助模型將訊息與角色關聯起來。",
216216 "None": "無",
217217 "character_names_default": "除了團體和過去的玩家角色人物外除了團體和過去的使用者角色外。否則,請確保在提示中提供名字。",
218218 "Don't add character names.": "不要新增角色人物名稱不要新增角色名稱",
219219 "Completion": "補充",
220220 "character_names_completion": "字元限制:僅限拉丁字母數字和底線。不適用於所有來源,特別是:Claude、MistralAI、Google。",
221221 "Add character names to completion objects.": "新增角色人物名稱來補充物件新增角色名稱來補充物件",
222222 "Message Content": "訊息內容",
223223 "Prepend character names to message contents.": "在訊息內容前新增角色人物名稱在訊息內容前新增角色名稱",
224224 "Continue Postfix": "繼續後綴",
225225 "The next chunk of the continued message will be appended using this as a separator.": "繼續訊息的下一塊將使用此作為分隔符附加",
226226 "Space": "空格",
@@ -233,37 +233,37 @@
233233 "Continue prefill": "繼續預先填充",
234234 "Continue sends the last message as assistant role instead of system message with instruction.": "繼續將最後的訊息作為助理角色發送,而不是帶有指令的系統訊息。",
235235 "Squash system messages": "合併系統訊息",
236236 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括範例對話)(不包括對話範例)。可能會提高某些模型的一致性。",
237237 "Enable function calling": "啟用函數調用",
238238 "Send inline images": "發送內嵌圖片",
239239 "image_inlining_hint_1": "如果模型支援(例如: GPT-4V、Claude 3 或 Llava 13B),則在提示詞中發送圖片。\n使用任何訊息上的",
240240 "image_inlining_hint_2": "動作或",
241241 "image_inlining_hint_3": "選單來附加圖片文件到聊天中。",
242242 "Inline Image Quality": "內嵌圖片品質",
243243 "openai_inline_image_quality_auto": "自動",
244244 "openai_inline_image_quality_low": "低",
245245 "openai_inline_image_quality_high": "高",
246246 "Use AI21 Tokenizer": "使用 AI21 符記器分詞器",
247247 "Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "對於 Jurassic 模型使用適當的符記器模型使用適當的分詞器,比 GPT 的更高效",
248248 "Use Google Tokenizer": "使用 Google 符記器分詞器",
249249 "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "通過 Google 模型的 API 使用適當的符記器使用適當的分詞器。提示詞處理速度較慢,但提供更準確的符記計數但提供更準確的符元計數。",
250250 "Use system prompt": "使用系統提示詞",
251251 "(Gemini 1.5 Pro/Flash only)": "(僅限於 Gemini 1.5 Pro/Flash)",
252252 "Merges_all_system_messages_desc_1": "合併所有系統訊息,直到第一條非系統角色的訊息直到第一則非系統角色的訊息,並通過 google 的",
253253 "Merges_all_system_messages_desc_2": "字段發送,而不是與其餘提示詞內容一起發送。",
254254 "Assistant Prefill": "預先填充助理訊息",
255255 "Start Claude's answer with...": "開始 Claude 的回答...",
256256 "Assistant Impersonation Prefill": "助理扮演時的預先填充",
257257 "Use system prompt (Claude 2.1+ only)": "使用系統提示詞(僅限 Claude 2.1+)",
258258 "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "為支援的模型發送系統提示詞。停用時,使用者訊息將新增到提示詞的開頭。",
259259 "User first message": "使用者第一條訊息使用者第一則訊息",
260260 "Restore User first message": "還原使用者第一個訊息還原使用者第一則訊息",
261261 "Human message": "人類訊息、指令等。\n當空白時不加入任何內容,也就是需要一個帶有使用者角色的新提示詞。",
262262 "New preset": "新預設",
263263 "Delete preset": "刪除預設",
264264 "View / Edit bias preset": "查看/編輯偏見預設/編輯 Bias 預設",
265265 "Add bias entry": "新增偏見條目新增 Bias 條目",
266266 "Most tokens have a leading space.": "大多數符記有前導空格大多數符元有前導空格",
267267 "API Connections": "API 連線",
268268 "Text Completion": "文字補充",
269269 "Chat Completion": "聊天補充",
@@ -274,14 +274,14 @@
274274 "Review the Privacy statement": "檢視隱私聲明",
275275 "Register a Horde account for faster queue times": "註冊Horde帳號以縮短排隊時間",
276276 "Learn how to contribute your idle GPU cycles to the Horde": "了解如何將閒置的 GPU 週期貢獻給 Horde",
277277 "Adjust context size to worker capabilities": "根據 worker 的能力調整上下文大小的能力調整上下文長度",
278278 "Adjust response length to worker capabilities": "根據 worker 的能力調整回應長度",
279279 "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "僅將已批准的 worker 排隊,可以幫助處理不良回應。可能會延長回應時間。",
280280 "Trusted workers only": "僅限受信任的 worker",
281281 "API key": "API 金鑰",
282282 "Get it here:": "在這裡獲取:",
283283 "Register": "登記註冊",
284284 "View my Kudos": "查看我的稱讚瀏覽我的讚賞記錄",
285285 "Enter": "輸入",
286286 "to use anonymous mode.": "以使用匿名模式。",
287287 "Clear your API key": "清除您的 API 金鑰",
@@ -289,7 +289,7 @@
289289 "Models": "模型",
290290 "Refresh models": "重新整理模型",
291291 "-- Horde models not loaded --": "-- Horde 模型未載入 --",
292292 "Not connected...": "尚未連線...",
293293 "API url": "API URL",
294294 "Example: http://127.0.0.1:5000/api ": "範例:http://127.0.0.1:5000/api ",
295295 "Connect": "連線",
@@ -298,9 +298,9 @@
298298 "Get your NovelAI API Key": "取得您的 NovelAI API 金鑰",
299299 "Enter it in the box below": "請在下面的框中輸入",
300300 "Novel AI Model": "NovelAI 模型",
301301 "No connection...": "沒有連線...",
302302 "API Type": "API 類型",
303303 "Default (completions compatible)": "預設(相容補充)",
304304 "TogetherAI API Key": "TogetherAI API 金鑰",
305305 "TogetherAI Model": "TogetherAI 模型",
306306 "-- Connect to the API --": "-- 連線到 API --",
@@ -317,10 +317,10 @@
317317 "Mancer Model": "Mancer 模型",
318318 "Make sure you run it with": "確保您使用以下方式執行它",
319319 "flag": "旗標",
320320 "API key (optional)": "API 金鑰(可選)",
321321 "Server url": "伺服器 URL",
322322 "Example: 127.0.0.1:5000": "範例:127.0.0.1:5000",
323323 "Custom model (optional)": "自訂模型(選填)",
324324 "vllm-project/vllm": "vllm-project/vllm",
325325 "vLLM API key": "vLLM API 金鑰",
326326 "Example: 127.0.0.1:8000": "範例:127.0.0.1:8000",
@@ -334,10 +334,10 @@
334334 "Ollama Model": "Ollama 模型",
335335 "Download": "下載",
336336 "Tabby API key": "Tabby API 金鑰",
337337 "koboldcpp API key (optional)": "KoboldCpp API 金鑰(可選)",
338338 "Example: 127.0.0.1:5001": "範例:127.0.0.1:5001",
339339 "Authorize": "授權",
340340 "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程取得您的 OpenRouter API 符記符元。您將被重新導向到 openrouter.ai",
341341 "Bypass status check": "繞過狀態檢查",
342342 "Chat Completion Source": "聊天補充來源",
343343 "Reverse Proxy": "反向代理伺服器",
@@ -348,11 +348,11 @@
348348 "Proxy Name": "代理伺服器名稱",
349349 "This will show up as your saved preset.": "這將顯示為您儲存的預設",
350350 "Proxy Server URL": "代理伺服器 URL",
351351 "Alternative server URL (leave empty to use the default value).": "替代伺服器 URL(留空以使用預設值)。",
352352 "Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "在此框中輸入任何內容之前,從API面板中刪除您的實際OAI API金鑰API 面板中刪除您的實際 OAI API 金鑰",
353353 "We cannot provide support for problems encountered while using an unofficial OpenAI proxy": "我們無法為使用非官方 OpenAI 代理伺服器時遇到的問題提供支援",
354354 "Doesn't work? Try adding": "不起作用?嘗試新增",
355355 "at the end!": "在最後!",
356356 "Proxy Password": "代理伺服器密碼",
357357 "Will be used as a password for the proxy instead of API key.": "將用作代理的密碼,而不是 API 金鑰",
358358 "Peek a password": "顯示密碼",
@@ -364,17 +364,17 @@
364364 "Use Proxy password field instead. This input will be ignored.": "請改用代理密碼欄位。此輸入將被忽略",
365365 "OpenAI Model": "OpenAI 模型",
366366 "Bypass API status check": "繞過 API 狀態檢查",
367367 "Show External models (provided by API)": "顯示外部模型(由 API 提供)",
368368 "Get your key from": "取得您的鑰匙",
369369 "Anthropic's developer console": "Anthropic 的開發者控制台",
370370 "Claude Model": "Claude 模型",
371371 "Window AI Model": "Window AI 模型",
372372 "Model Order": "模型順序",
373373 "Alphabetically": "按字母順序",
374374 "Price": "價格(最便宜的)",
375375 "Context Size": "上下文大小上下文長度",
376376 "Group by vendors": "按供應商分組",
377377 "Group by vendors Description": "將 OpenAI Anthropic 等等的模型放各自供應商的群組中等模型放各自供應商的群組中。可以與排序功能結合使用。",
378378 "Allow fallback routes": "允許備援路徑",
379379 "Allow fallback routes Description": "如果選擇的模型無法滿足要求,會自動選擇替代模型。",
380380 "Scale API Key": "Scale API 金鑰",
@@ -392,7 +392,7 @@
392392 "Perplexity Model": "Perplexity 模型",
393393 "Cohere API Key": "Cohere API 金鑰",
394394 "Cohere Model": "Cohere 模型",
395395 "Custom Endpoint (Base URL)": "自訂端點(Base URL)",
396396 "Custom API Key": "自訂 API 金鑰",
397397 "Available Models": "可用模型",
398398 "Prompt Post-Processing": "提示詞後處理",
@@ -412,7 +412,7 @@
412412 "Chat Start": "聊天開始符號",
413413 "Add Chat Start and Example Separator to a list of stopping strings.": "將聊天開始和範例分隔符號加入終止字串中。",
414414 "Use as Stop Strings": "用作停止字串",
415415 "context_allow_jailbreak": "如果在角色人物卡中定義了越獄如果在角色卡中定義了越獄,且啟用了「偏好角色人物卡越獄偏好角色卡越獄」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。",
416416 "Allow Jailbreak": "允許越獄",
417417 "Context Order": "上下文順序",
418418 "Summary": "摘要",
@@ -429,9 +429,9 @@
429429 "Activation Regex": "啟用正規表示式",
430430 "Wrap Sequences with Newline": "用換行符包裹序列",
431431 "Replace Macro in Sequences": "取代序列中的巨集",
432432 "Skip Example Dialogues Formatting": "跳過範例對話的格式設定跳過對話範例的格式設定",
433433 "Include Names": "包含名稱",
434434 "Force for Groups and Personas": "強制用於群組和玩家角色人物強制用於群組和使用者角色",
435435 "System Prompt": "系統提示詞",
436436 "Instruct Mode Sequences": "指示模式序列",
437437 "System Prompt Wrapping": "系統提示詞換行",
@@ -448,7 +448,7 @@
448448 "Assistant Message Prefix": "助理訊息前綴",
449449 "Inserted after an Assistant message.": "插入在助理訊息之後。",
450450 "Assistant Message Suffix": "助理訊息後綴",
451451 "Inserted before a System (added by slash commands or extensions) message.": "插入在系統(透過斜線命令或擴充套件增加透過斜線命令或擴充功能增加)訊息之前。",
452452 "System Message Prefix": "系統訊息前綴",
453453 "Inserted after a System message.": "插入在系統訊息之後",
454454 "System Message Suffix": "系統訊息後綴",
@@ -457,25 +457,25 @@
457457 "Misc. Sequences": "其他序列",
458458 "Inserted before the first Assistant's message.": "插入在第一個助理的訊息之前。",
459459 "First Assistant Prefix": "首個助理前綴",
460460 "instruct_last_output_sequence": "插入在最後一個助理的訊息之前,或在生成 AI 回覆時作為最後一行提示詞(除了中立/系統角色)。",
461461 "Last Assistant Prefix": "末尾助理前綴",
462462 "Will be inserted as a last prompt line when using system/neutral generation.": "在使用系統/中立生成時作為最後一行提示詞插入。",
463463 "System Instruction Prefix": "系統指令前綴",
464464 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果生成了停止序列,包括該序列以及之後的所有內容將從輸出中刪除。",
465465 "Stop Sequence": "停止序列",
466466 "Will be inserted at the start of the chat history if it doesn't start with a User message.": "如果聊天歷史不是以使用者訊息開始,將在聊天歷史的開頭插入。",
467467 "User Filler Message": "使用者填充訊息",
468468 "Context Formatting": "上下文格式",
469469 "(Saved to Context Template)": "(已儲存到上下文範本)",
470470 "Always add character's name to prompt": "總是將角色人物名稱新增到提示詞中總是將角色名稱新增到提示詞中",
471471 "Generate only one line per request": "每次請求僅生成一行",
472472 "Trim Incomplete Sentences": "修剪不完整的句子",
473473 "Include Newline": "包含換行符號",
474474 "Misc. Settings": "其他設定",
475475 "Collapse Consecutive Newlines": "折疊連續的換行符號",
476476 "Trim spaces": "修剪空格",
477477 "Tokenizer": "符記化工具分詞器 Tokenizer",
478478 "Token Padding": "符記填充符元填充",
479479 "Start Reply With": "開始回覆",
480480 "AI reply prefix": "AI 回覆前綴",
481481 "Show reply prefix in chat": "在聊天中顯示回覆前綴",
@@ -486,25 +486,25 @@
486486 "Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集",
487487 "Auto-Continue": "自動繼續",
488488 "Allow for Chat Completion APIs": "允許聊天補充 API",
489489 "Target length (tokens)": "目標長度(符記)(符元)",
490490 "World Info": "世界資訊",
491491 "Locked = World Editor will stay open": "上鎖 = 世界編輯器將保持開啟",
492492 "Worlds/Lorebooks": "世界/知識書",
493493 "Active World(s) for all chats": "所有聊天啟用中的世界書",
494494 "-- World Info not found --": "-- 未找到世界資訊 --",
495495 "Global World Info/Lorebook activation settings": "全球世界資訊/傳說書啟動設置全域世界資訊/知識書啟動設定",
496496 "Click to expand": "點擊展開",
497497 "Scan Depth": "掃描深度",
498498 "Context %": "上下文百分比",
499499 "Budget Cap": "預算上限",
500500 "(0 = disabled)": "(0 = 停用)",
501501 "Scan chronologically until reached min entries or token budget.": "按時間順序掃描直到達到最小條目或符記預算按時間順序掃描直到達到最小條目或符元預算",
502502 "Min Activations": "最小啟動次數",
503503 "Max Depth": "最大深度",
504504 "(0 = unlimited, use budget)": "(0 = 無限制, 使用預算)",
505505 "Insertion Strategy": "插入策略",
506506 "Sorted Evenly": "均等排序",
507507 "Character Lore First": "角色人物知識書優先角色知識書優先",
508508 "Global Lore First": "全域知識書優先",
509509 "Entries can activate other entries by mentioning their keywords": "條目可以通過提及其關鍵字來啟用其他條目",
510510 "Recursive Scan": "遞迴掃描",
@@ -523,19 +523,19 @@
523523 "Open all Entries": "開啟所有條目",
524524 "Close all Entries": "關閉所有條目",
525525 "New Entry": "新條目",
526526 "Fill empty Memo/Titles with Keywords": "用關鍵字填充空的備忘錄/標題",
527527 "Import World Info": "匯入世界資訊",
528528 "Export World Info": "匯出世界資訊",
529529 "Duplicate World Info": "複製世界資訊",
530530 "Delete World Info": "刪除世界資訊",
531531 "Search...": "搜尋...",
532532 "Search": "搜尋",
533533 "Priority": "優先順序",
534534 "Custom": "自訂",
535535 "Title A-Z": "標題 A-Z",
536536 "Title Z-A": "標題 Z-A",
537537 "Tokens ↗": "符記符元 ↗",
538538 "Tokens ↘": "符記符元 ↘",
539539 "Depth ↗": "深度 ↗",
540540 "Depth ↘": "深度 ↘",
541541 "Order ↗": "順序 ↗",
@@ -553,12 +553,12 @@
553553 "Admin Panel": "管理面板",
554554 "Logout": "登出",
555555 "Search Settings": "搜尋設定",
556556 "UI Theme": "介面佈景主題介面主題",
557557 "Import a theme file": "匯入布景主題檔案匯入主題檔案",
558558 "Export a theme file": "匯出布景主題檔案匯出主題檔案",
559559 "Delete a theme": "刪除主題",
560560 "Update a theme file": "更新主題檔案",
561561 "Save as a new theme": "儲存成新的布景主題儲存成新的主題",
562562 "Avatar Style": "頭像樣式",
563563 "Circle": "圓形",
564564 "Square": "方形",
@@ -568,7 +568,7 @@
568568 "Bubbles": "氣泡",
569569 "Document": "文件",
570570 "Specify colors for your theme.": "為您的主題指定顏色",
571571 "Theme Colors": "佈景主題顏色介面主題顏色",
572572 "Main Text": "主要文字",
573573 "Italics Text": "斜體文字",
574574 "Underlined Text": "帶底線的文字",
@@ -588,15 +588,15 @@
588588 "Text Shadow Width": "文字陰影寬度",
589589 "Strength of the text shadows": "文字陰影的強度",
590590 "Disables animations and transitions": "停用動畫和過渡效果",
591591 "Reduced Motion": "減少動作減少動畫效果",
592592 "removes blur from window backgrounds": "從視窗背景中移除模糊效果,以加快算繪速度。",
593593 "No Blur Effect": "無模糊效果",
594594 "Remove text shadow effect": "移除文字陰影效果。",
595595 "No Text Shadows": "無文字陰影",
596596 "Reduce chat height, and put a static sprite behind the chat window": "減少聊天高度,並在聊天視窗後放置一個靜態 Sprite。",
597597 "Waifu Mode": "Waifu 模式視覺小說模式",
598598 "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "始終顯示聊天訊息的訊息動作上下文項目的完整列表,而不是將它們隱藏在「...」後面。",
599599 "Auto-Expand Message Actions": "自動展開訊息動作展開訊息快速編輯選單",
600600 "Alternative UI for numeric sampling parameters with fewer steps": "數值取樣參數的替代 UI,步驟更少",
601601 "Zen Sliders": "Zen 滑桿",
602602 "Entirely unrestrict all numeric sampling parameters": "完全解除所有數值取樣參數的限制。",
@@ -611,36 +611,36 @@
611611 "Message IDs": "訊息 ID",
612612 "Hide avatars in chat messages.": "在聊天訊息中隱藏頭像",
613613 "Hide Chat Avatars": "隱藏聊天頭像",
614614 "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符記數量在聊天記錄中顯示每條訊息中的符元數量",
615615 "Show Message Token Count": "顯示訊息符記數量顯示訊息符元數量",
616616 "Single-row message input area. Mobile only, no effect on PC": "單行訊息輸入區域。僅適用於行動裝置版。",
617617 "Compact Input Area (Mobile)": "緊湊的輸入區域(行動版)",
618618 "In the Character Management panel, show quick selection buttons for favorited characters": "在角色人物管理面板中在角色管理面板中顯示加入到最愛角色人物的快速選擇按鈕顯示加入到最愛角色的快速選擇按鈕。",
619619 "Characters Hotswap": "角色人物快速選擇角色卡快捷選單",
620620 "Enable magnification for zoomed avatar display.": "啟用放大顯示頭像",
621621 "Avatar Hover Magnification": "頭像懸停時的放大倍數",
622622 "Enables a magnification effect on hover when you display the zoomed avatar after clicking an avatar's image in chat.": "當你在聊天中點選頭像的圖片後,這會啟用滑鼠懸停時的放大效果。",
623623 "Show tagged character folders in the character list": "在角色人物列表中顯示標籤角色人物資料夾在角色列表中顯示標籤角色資料夾。",
624624 "Tags as Folders": "標籤作為資料夾",
625625 "Tags_as_Folders_desc": "最近更改:標籤必須在標籤管理選單中標記為資料夾才能顯示為此類。點選這裡打開。",
626626 "Character Handling": "角色人物處理角色處理",
627627 "If set in the advanced character definitions, this field will be displayed in the characters list.": "如果在進階角色人物定義中設定如果在進階角色定義中設定這個欄位將顯示在角色人物清單中這個欄位將顯示在角色清單中。",
628628 "Char List Subheader": "角色人物列表子標題角色列表子標題",
629629 "Character Version": "角色人物版本角色版本",
630630 "Created by": "建立者",
631631 "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊配對,並通過所有資料欄位在列表中搜尋角色人物並通過所有資料欄位在列表中搜尋角色,而不僅僅是通過名稱子字串。",
632632 "Advanced Character Search": "進階角色人物搜尋進階角色搜尋",
633633 "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果選中並且角色人物卡包含提示詞覆寫(如果選中並且角色卡包含提示詞覆寫(系統提示詞),則使用該提示詞。",
634634 "Prefer Character Card Prompt": "偏好角色人物卡提示詞偏好角色卡主要提示詞",
635635 "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色人物卡包含越獄覆寫(歷史指示後)如果選中並且角色卡包含越獄覆寫(聊天歷史後指示),則使用該提示詞。",
636636 "Prefer Character Card Jailbreak": "偏好角色人物卡越獄偏好角色卡越獄",
637637 "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色人物圖像大小避免裁剪和調整匯入的角色圖像大小。未勾選時將會裁剪/調整大小到 512x768。",
638638 "Never resize avatars": "永不調整頭像大小",
639639 "Show actual file names on the disk, in the characters list display only": "僅在角色人物列表顯示實際檔案名稱僅在角色列表顯示實際檔案名稱。",
640640 "Show avatar filenames": "顯示頭像檔案名",
641641 "Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored": "在角色人物匯入時提示詞匯入嵌入的卡片標籤在角色匯入時提示詞匯入嵌入的卡片標籤。否則,嵌入的標籤將被忽略。",
642642 "Import Card Tags": "匯入卡片中的標籤",
643643 "Hide character definitions from the editor panel behind a spoiler button": "在編輯器面板中將角色人物定義隱藏在劇透按鈕後面在編輯器面板中將角色定義隱藏在劇透按鈕後面。",
644644 "Spoiler Free Mode": "無劇透模式",
645645 "Miscellaneous": "其他",
646646 "Reload and redraw the currently open chat": "重新載入並重繪目前開啟的聊天",
@@ -653,26 +653,26 @@
653653 "Play a sound when a message generation finishes": "訊息生成完成時播放音效。",
654654 "Message Sound": "訊息音效",
655655 "Only play a sound when ST's browser tab is unfocused": "僅在 ST 的瀏覽器分頁未聚焦時播放音效。",
656656 "Background Sound Only": "僅背景音效僅作為背景音效",
657657 "Reduce the formatting requirements on API URLs": "降低 API URL 的格式要求。",
658658 "Relaxed API URLS": "寬鬆的 API URL 格式",
659659 "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "當新角色人物含有知識書時當新角色含有知識書時,詢問是否要匯入嵌入的世界資訊/知識書。如果未選中,則會顯示簡短的訊息。",
660660 "Lorebook Import Dialog": "匯入知識書對話框",
661661 "Restore unsaved user input on page refresh": "在頁面刷新時還原未儲存的使用者輸入。",
662662 "Restore User Input": "還原使用者輸入",
663663 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允許通過拖動重新定位某些 UI 元素。僅適用於 PC 版。",
664664 "Movable UI Panels": "可移動的 UI 面板",
665665 "MovingUI preset. Predefined/saved draggable positions": "MovingUI 預設。預先定義/儲存可拖動位置。",
666666 "MUI Preset": "MUI 預設",
667667 "Save movingUI changes to a new file": "將移動UI變更儲存到新檔案將移動 UI 變更儲存到新檔案",
668668 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置",
669669 "Apply a custom CSS style to all of the ST GUI": "將自訂 CSS 樣式應用於所有 ST GUI",
670670 "Custom CSS": "自訂 CSS",
671671 "Expand the editor": "展開編輯器",
672672 "Chat/Message Handling": "聊天/訊息處理",
673673 "# Messages to Load": "每頁載入的訊息數",
674674 "The number of chat history messages to load before pagination.": "每頁載入的聊天歷史訊息數",
675675 "(0 = All)": "(0 = 全部)",
676676 "Streaming FPS": "串流 FPS",
677677 "Update speed of streamed text.": "更新串流文字的速度",
678678 "Example Messages Behavior": "訊息行為範例:",
@@ -681,10 +681,10 @@
681681 "Never include examples": "永不包含範例",
682682 "Send on Enter": "按下 Enter 鍵發送:",
683683 "Disabled": "停用",
684684 "Automatic (PC)": "自動(PC)",
685685 "Press Send to continue": "按下傳送繼續",
686686 "Show a button in the input area to ask the AI to continue (extend) its last message": "在輸入區域顯示一個按鈕,請求 AI 繼續(擴充)其最後一條訊息)其最後一則訊息",
687687 "Quick 'Continue' button": "快速「繼續繼續回應」按鈕",
688688 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在最後的聊天訊息中顯示箭頭按鈕,以生成替代的 AI 回應。適用於 PC 和行動裝置版",
689689 "Swipes": "滑動",
690690 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允許在最後的聊天訊息上使用滑動手勢以觸發滑動生成。僅適用於行動裝置版。",
@@ -704,8 +704,8 @@
704704 "Allow AI messages in groups to contain lines spoken by other group members": "允許群組中的 AI 訊息包含其他群組成員說的話",
705705 "Relax message trim in Groups": "放寬群組中的訊息修剪",
706706 "Log prompts to console": "將提示詞記錄到控制台",
707707 "Requests logprobs from the API for the Token Probabilities feature": "從 API 請求 logprobs 用於符記機率功能用於符元機率功能。",
708708 "Request token probabilities": "請求符記機率請求符元機率",
709709 "Automatically reject and re-generate AI message based on configurable criteria": "根據可配置標準自動拒絕並重新生成 AI 訊息。",
710710 "Auto-swipe": "自動滑動",
711711 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",
@@ -722,62 +722,62 @@
722722 "Starts with": "開始於",
723723 "Includes": "包含",
724724 "Fuzzy": "模糊",
725725 "Sets the style of the autocomplete.": "設置自動完成的樣式設定自動完成的樣式",
726726 "Autocomplete Style": "自動完成樣式",
727727 "Follow Theme": "參照佈景主題參照介面主題",
728728 "Dark": "深色",
729729 "Sets the font size of the autocomplete.": "設置自動完成的字體大小設定自動完成的字體大小",
730730 "Sets the width of the autocomplete.": "設置自動完成的寬度設定自動完成的寬度",
731731 "Autocomplete Width": "自動完成寬度",
732732 "chat input box": "聊天輸入框",
733733 "entire chat width": "整個聊天寬度",
734734 "full window width": "全視窗寬度",
735735 "STscript Settings": "STscript 設定",
736736 "Sets default flags for the STscript parser.": "設置設定 STscript 解析器的預設標誌",
737737 "Parser Flags": "解析器標誌",
738738 "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切換到更嚴格的字元跳脫,允許所有分隔符號使用反斜線跳脫,反斜線自己也可以跳脫。",
739739 "STRICT_ESCAPING": "STRICT_ESCAPING",
740740 "Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.": "將所有 {{getvar::}} 和 {{getglobalvar::}} 巨集取代為作用域變量以避免雙重巨集取代",
741741 "REPLACE_GETVAR": "REPLACE_GETVAR",
742742 "Change Background Image": "更換背景圖片變更背景圖片",
743743 "Filter": "篩選",
744744 "Automatically select a background based on the chat context": "根據聊天上下文自動選擇背景",
745745 "Auto-select": "自動選擇",
746746 "System Backgrounds": "系統背景圖片",
747747 "Chat Backgrounds": "聊天背景圖片",
748748 "bg_chat_hint_1": "使用擴充套件使用擴充功能",
749749 "bg_chat_hint_2": "所產生的背景圖片將會出現在這裡。",
750750 "Extensions": "擴充套件擴充功能",
751751 "Notify on extension updates": "擴充套件更新時通知擴充功能更新時通知",
752752 "Manage extensions": "管理擴充套件管理擴充功能",
753753 "Import Extension From Git Repo": "從 Git 倉庫匯入擴充套件倉庫匯入擴充功能",
754754 "Install extension": "安裝擴充套件安裝擴充功能",
755755 "Extras API:": "Extras API:",
756756 "Auto-connect": "自動連線",
757757 "Extras API URL": "Extras API URL",
758758 "Extras API key (optional)": "Extras API 金鑰(選填)",
759759 "Persona Management": "玩家角色人物管理使用者角色管理",
760760 "How do I use this?": "我該如何使用這個?",
761761 "Click for stats!": "點選查看統計!",
762762 "Usage Stats": "使用統計",
763763 "Backup your personas to a file": "將您的玩家角色人物備份到檔案中將您的使用者角色備份到檔案中",
764764 "Backup": "備份",
765765 "Restore your personas from a file": "從檔案還原您的角色人物從檔案還原您的角色",
766766 "Restore": "還原",
767767 "Create a dummy persona": "建立一個假玩家角色人物建立一個虛構使用者角色",
768768 "Create": "建立",
769769 "Toggle grid view": "切換網格視圖切換為網格視圖",
770770 "No persona description": "無玩家角色人物描述無使用者角色描述",
771771 "Name": "名稱",
772772 "Enter your name": "輸入您的名字",
773773 "Click to set a new User Name": "點選以設置新使用者名稱點選以設定新使用者名稱",
774774 "Click to lock your selected persona to the current chat. Click again to remove the lock.": "點選以將所選玩家角色人物上鎖到目前聊天點選以將所選使用者角色上鎖到目前聊天。再次點選以移除上鎖。",
775775 "Click to set user name for all messages": "點選以設置所有訊息的使用者名稱點選以設定所有訊息的使用者名稱",
776776 "Persona Description": "玩家角色人物描述使用者角色描述",
777777 "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "範例:[{{user}} 是一個 28 歲的羅馬尼亞貓娘。]",
778778 "Tokens persona description": "符記角色人物描述符元角色描述",
779779 "Position:": "位置:",
780780 "In Story String / Prompt Manager": "在故事字串 / 提示詞管理器",
781781 "Top of Author's Note": "作者備註的頂部",
782782 "Bottom of Author's Note": "作者備註的底部",
783783 "In-chat @ Depth": "聊天中 @ 深度",
@@ -786,68 +786,68 @@
786786 "System": "系統",
787787 "User": "使用者",
788788 "Assistant": "助理",
789789 "Show notifications on switching personas": "切換角色人物時顯示通知切換角色時顯示通知",
790790 "Character Management": "角色人物管理角色管理",
791791 "Locked = Character Management panel will stay open": "上鎖 = 角色人物管理面板將保持開啟角色管理面板將保持開啟",
792792 "Select/Create Characters": "選擇/建立角色人物/建立角色",
793793 "Favorite characters to add them to HotSwaps": "將角色人物加入到最愛來新增到快速切換將角色加入到最愛來新增到快速切換",
794794 "Token counts may be inaccurate and provided just for reference.": "符記計數可能不準確符元計數可能不準確,僅供參考。",
795795 "Total tokens": "符記總計符元總計",
796796 "Calculating...": "計算中...",
797797 "Tokens": "符記符元",
798798 "Permanent tokens": "永久符記永久符元",
799799 "Permanent": "永久",
800800 "About Token 'Limits'": "關於符記數關於符元數「限制」",
801801 "Toggle character info panel": "切換角色人物資訊面板切換角色資訊面板",
802802 "Name this character": "為此角色人物命名為此角色命名",
803803 "extension_token_counter": "符記符元:",
804804 "Click to select a new avatar for this character": "點選以選擇此角色人物的新頭像點選以選擇此角色的新頭像",
805805 "Add to Favorites": "新增至我的最愛",
806806 "Advanced Definition": "進階定義",
807807 "Character Lore": "角色人物知識書角色知識書",
808808 "Chat Lore": "聊天知識",
809809 "Export and Download": "匯出和下載",
810810 "Duplicate Character": "複製角色人物複製角色",
811811 "Create Character": "建立角色人物建立角色",
812812 "Delete Character": "刪除角色人物刪除角色",
813813 "More...": "更多...",
814814 "Link to World Info": "連結到世界資訊",
815815 "Import Card Lore": "匯入卡片中的知識書",
816816 "Scenario Override": "情境覆寫",
817817 "Convert to Persona": "轉換為玩家角色人物轉換為使用者角色",
818818 "Rename": "重新命名",
819819 "Link to Source": "連結到來源",
820820 "Replace / Update": "取代 / 更新",
821821 "Import Tags": "匯入標籤",
822822 "Search / Create Tags": "搜尋/建立標籤",
823823 "View all tags": "查看所有標籤",
824824 "Creator's Notes": "建立者備註",
825825 "Show / Hide Description and First Message": "顯示/隱藏描述和第一條訊息/隱藏描述和第一則訊息",
826826 "Character Description": "角色人物描述角色描述",
827827 "Click to allow/forbid the use of external media for this character.": "點選以允許/禁止此角色人物使用外部媒體/禁止此角色使用外部媒體",
828828 "Ext. Media": "外部媒體權限",
829829 "Describe your character's physical and mental traits here.": "在此描述角色人物的身體和心理特徵在此描述角色的身體和心理特徵。",
830830 "First message": "初始訊息",
831831 "Click to set additional greeting messages": "點選以設置額外的問候訊息點選以設定額外的問候訊息",
832832 "Alt. Greetings": "額外問候語",
833833 "This will be the first message from the character that starts every chat.": "這將是每次聊天開始時角色人物發送的第一條訊息這將是每次聊天開始時角色發送的第一則訊息。",
834834 "Group Controls": "群組控制",
835835 "Chat Name (Optional)": "聊天名稱(選填)",
836836 "Click to select a new avatar for this group": "點選以選擇此群組的新頭像",
837837 "Group reply strategy": "群組回應策略",
838838 "Natural order": "自然順序",
839839 "List order": "清單順序",
840840 "Group generation handling mode": "群組生成處理模式",
841841 "Swap character cards": "交換角色人物卡交換角色卡",
842842 "Join character cards (exclude muted)": "加入角色人物卡(加入角色卡(排除靜音)",
843843 "Join character cards (include muted)": "加入角色人物卡(加入角色卡(包括靜音)",
844844 "Inserted before each part of the joined fields.": "插入在合併欄位的每一部分之前。",
845845 "Join Prefix": "加入前綴",
846846 "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "當選擇加入角色卡時,角色的所有對應欄位將連接在一起。\r這意味著,例如在故事字串中,所有角色描述都將連接到一個大文字中。\r如果您希望分隔這些字段,可以在此處定義前綴或後綴。\r\r該值支援普通宏,並且還將 {{char}} 替換為相關字元的名稱,並將 <FIELDNAME> 替換為部分的名稱(例如:描述、個性、場景等)",
847847 "Inserted after each part of the joined fields.": "插入在合併欄位的每一部分之後。",
848848 "Join Suffix": "加入後綴",
849849 "Set a group chat scenario": "設定群組聊天情境",
850850 "Click to allow/forbid the use of external media for this group.": "點選以允許/禁止此群組使用外部媒體",
851851 "Restore collage avatar": "還原預設頭像",
852852 "Allow self responses": "允許自我回應",
853853 "Auto Mode": "自動模式",
@@ -855,11 +855,11 @@
855855 "Hide Muted Member Sprites": "隱藏靜音成員精靈",
856856 "Current Members": "目前成員",
857857 "Add Members": "新增成員",
858858 "Create New Character": "建立新角色人物建立新角色",
859859 "Import Character from File": "從檔案匯入角色人物從檔案匯入角色",
860860 "Import content from external URL": "從外部 URL 匯入內容",
861861 "Create New Chat Group": "建立新聊天群組",
862862 "Characters sorting order": "角色人物排序依據角色排序依據",
863863 "A-Z": "A-Z",
864864 "Z-A": "Z-A",
865865 "Newest": "最新",
@@ -868,13 +868,13 @@
868868 "Recent": "最近",
869869 "Most chats": "最多聊天",
870870 "Least chats": "最少聊天",
871871 "Most tokens": "最多符記最多符元",
872872 "Least tokens": "最少符記最少符元",
873873 "Random": "隨機",
874874 "Toggle character grid view": "切換角色人物網格視圖切換為角色網格視圖",
875875 "Bulk_edit_characters": "批量編輯角色人物批量編輯角色\n\n點選以切換角色人物n點選以切換角色\nShift + 點選以選擇/取消選擇一範圍的角色人物/取消選擇一範圍的角色\n右鍵以查看動作",
876876 "Bulk select all characters": "全選所有角色人物全選所有角色",
877877 "Bulk delete characters": "批量刪除角色人物批量刪除角色",
878878 "popup-button-save": "儲存",
879879 "popup-button-yes": "是",
880880 "popup-button-no": "否",
@@ -882,40 +882,40 @@
882882 "popup-button-import": "匯入",
883883 "Advanced Defininitions": "- 進階定義",
884884 "Prompt Overrides": "提示詞覆寫",
885885 "(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)",
886886 "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。",
887887 "Main Prompt": "主要提示詞",
888888 "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此處的任何內容將取代此角色人物使用的預設主要提示詞此處的任何內容將取代此角色使用的預設主要提示詞(v2 規範:system_prompt)",
889889 "Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "此處的任何內容將取代此角色人物使用的預設越獄提示詞此處的任何內容將取代此角色使用的預設越獄提示詞(v2 規範:post_history_instructions)",
890890 "Creator's Metadata (Not sent with the AI prompt)": "建立者的中繼資料(不會與 AI 提示詞一起發送)",
891891 "Creator's Metadata": "建立者的中繼資料",
892892 "(Not sent with the AI Prompt)": "(不與 AI 提示詞一起發送)",
893893 "Everything here is optional": "此處所有內容均為選填",
894894 "(Botmaker's name / Contact Info)": "(機器人製作者的名字 / 聯繫資訊)(機器人建立者的名字/聯絡資訊)",
895895 "(If you want to track character versions)": "(如果您想追蹤角色人物版本)(若您想追蹤角色版本)",
896896 "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述機器人,提供使用技巧,或列出已測試的聊天模型。這將顯示在角色人物列表中這將顯示在角色列表中)",
897897 "Tags to Embed": "嵌入標籤",
898898 "(Write a comma-separated list of tags)": "(使用逗號分隔每個標籤)",
899899 "Personality summary": "個性摘要",
900900 "(A brief description of the personality)": "(個性的簡短描述)(關於角色性格的簡要描述)",
901901 "Scenario": "情境情境設想",
902902 "(Circumstances and context of the interaction)": "(互動的情況和上下文)(互動情形與聊天背景)",
903903 "Character's Note": "角色人物筆記角色筆記",
904904 "(Text to be inserted in-chat @ designated depth and role)": "(要在聊天中插入的文字 @ 指定深度和角色)",
905905 "@ Depth": "@ 深度",
906906 "Role": "角色",
907907 "Talkativeness": "健談度",
908908 "How often the character speaks in group chats!": "角色人物在群組聊天中說話的頻率角色在群組聊天中說話的頻率!",
909909 "How often the character speaks in": "角色說話的頻率",
910910 "group chats!": "群組聊天!",
911911 "Shy": "害羞寡言",
912912 "Normal": "正常",
913913 "Chatty": "健談",
914914 "Examples of dialogue": "對話範例",
915915 "Important to set the character's writing style.": "設定角色人物的寫作樣式很重要設定角色的寫作樣式很重要。",
916916 "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)",
917917 "Save": "儲存",
918918 "Chat History": "聊天紀錄聊天記錄",
919919 "Import Chat": "匯入聊天",
920920 "Copy to system backgrounds": "複製到系統背景圖片",
921921 "Rename background": "重新命名背景圖片",
@@ -924,18 +924,18 @@
924924 "Delete background": "刪除背景圖片",
925925 "Chat Scenario Override": "聊天情境覆寫",
926926 "Remove": "刪除",
927927 "Type here...": "在此輸入...",
928928 "Chat Lorebook": "聊天知識書",
929929 "Chat Lorebook for": "聊天知識書",
930930 "chat_world_template_txt": "選定的世界資訊將附加到此聊天。\n在生成 AI 回覆時,它將與全域和角色知識書中的條目結合。",
931931 "Select a World Info file for": "選擇世界資訊檔案",
932932 "Primary Lorebook": "主要知識書",
933933 "A selected World Info will be bound to this character as its own Lorebook.": "選定的世界資訊將作為此角色人物的知識書附加到此角色人物選定的世界資訊將作為此角色的知識書附加到此角色。",
934934 "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "生成 AI 回覆時,將與全域世界資訊選擇器中的條目結合。",
935935 "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "匯出角色人物時匯出角色時,也會匯出嵌入 JSON 資料中的選定知識書檔案。",
936936 "Additional Lorebooks": "額外的知識書",
937937 "Associate one or more auxillary Lorebooks with this character.": "將一個或多個輔助知識書與此角色人物關聯將一個或多個輔助知識書與此角色關聯。",
938938 "NOTE: These choices are optional and won't be preserved on character export!": "注意:這些選項是選填的,在角色人物匯出時不會保留在角色匯出時不會保留!",
939939 "Rename chat file": "重新命名聊天檔案",
940940 "Export JSONL chat file": "匯出 JSONL 聊天檔案",
941941 "Download chat as plain text document": "將聊天下載為純文字檔案",
@@ -943,15 +943,15 @@
943943 "Drag to reorder tag": "拖動以重新排序標籤",
944944 "Use tag as folder": "將標籤用作資料夾",
945945 "Delete tag": "刪除標籤",
946946 "Entry Title/Memo": "條目標題/備註",
947947 "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled": "WI條目狀態世界資訊條目狀態:🔵常數 🟢正常 🔗向量 ❌停用",
948948 "WI_Entry_Status_Constant": "🔵",
949949 "WI_Entry_Status_Normal": "🟢",
950950 "WI_Entry_Status_Vectorized": "🔗",
951951 "WI_Entry_Status_Disabled": "❌",
952952 "T_Position": "↑角色人物角色角色人物定義之前角色定義之前\n↓角色人物角色角色人物定義之後角色定義之後\n↑備註:作者備註之前\n↓備註:作者備註之後\n@深度",
953953 "Before Char Defs": "角色人物定義之前角色定義之前",
954954 "After Char Defs": "角色人物定義之後角色定義之後",
955955 "Before EM": "範例訊息之前",
956956 "After EM": "範例訊息之後",
957957 "Before AN": "作者備註之前",
@@ -962,11 +962,11 @@
962962 "Depth": "深度",
963963 "Order:": "順序:",
964964 "Order": "順序",
965965 "Trigger %:": "觸發%",
966966 "Probability": "機率",
967967 "Duplicate world info entry": "複製世界資訊物件",
968968 "Delete world info entry": "刪除世界資訊物件",
969969 "Comma separated (required)": "逗號分隔(必填)",
970970 "Primary Keywords": "主要關鍵字",
971971 "Keywords or Regexes": "關鍵字或正規表示式",
972972 "Comma separated list": "逗號分隔列表",
@@ -976,27 +976,27 @@
976976 "AND ALL": "AND 所有",
977977 "NOT ALL": "NOT 所有",
978978 "NOT ANY": "NOT 任何",
979979 "(ignored if empty)": "(如果為空則忽略)",
980980 "Optional Filter": "選填過濾器",
981981 "Keywords or Regexes (ignored if empty)": "關鍵字或正規表示式(如果為空則忽略)",
982982 "Comma separated list (ignored if empty)": "逗號分隔列表(如果為空則忽略)",
983983 "Use global setting": "使用全域設定",
984984 "Case-Sensitive": "區分大小寫",
985985 "Yes": "是",
986986 "No": "否",
987987 "Can be used to automatically activate Quick Replies": "可用於自動啟用快速回覆",
988988 "Automation ID": "自動化 ID",
989989 "( None )": "( )",
990990 "Content": "內容",
991991 "Exclude from recursion": "不可遞迴(此條目不會被其他條目啟用)",
992992 "Prevent further recursion (this entry will not activate others)": "防止進一步遞迴(此條目不會啟用其他條目)",
993993 "Delay until recursion (this entry can only be activated on recursive checking)": "延遲遞迴(此條目只能在遞迴檢查時啟用)",
994994 "What this keyword should mean to the AI, sent verbatim": "這個關鍵字對 AI 應意味著什麼,逐字發送",
995995 "Filter to Character(s)": "角色人物篩選角色篩選",
996996 "Character Exclusion": "角色人物排除角色排除",
997997 "-- Characters not found --": "-- 未找到角色人物未找到角色 --",
998998 "Inclusion Group": "包含的群組",
999999 "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "如果觸發多個條目,包含群組可確保一次僅啟動一組中的一個條目。\r支援多個以逗號分隔的群組。\r\r文件:世界資訊 - ——包容性集團",
10001000 "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "優先考慮此條目:選取後,此條目將在所有選擇中優先。\r如果有多個優先級,則選擇「順序」最高的一個。",
10011001 "Only one entry with the same label will be activated": "僅會啟用具有相同標籤的一個條目",
10021002 "A relative likelihood of entry activation within the group": "群組內條目啟用的相對可能性",
@@ -1004,7 +1004,7 @@
10041004 "Selective": "選擇性",
10051005 "Use Probability": "使用機率",
10061006 "Add Memo": "新增備忘錄",
10071007 "Text or token ids": "文字或符記文字或符元 ID",
10081008 "close": "關閉",
10091009 "prompt_manager_edit": "編輯",
10101010 "prompt_manager_name": "名稱",
@@ -1015,7 +1015,7 @@
10151015 "Injection position. Next to other prompts (relative) or in-chat (absolute).": "注入位置。與其他提示詞相鄰(相對位置)或在聊天中(絕對位置)。",
10161016 "prompt_manager_relative": "相對位置",
10171017 "prompt_manager_depth": "深度",
10181018 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最後一條訊息之後在最後一則訊息之後,1 = 在最後一條訊息之前在最後一則訊息之前,以此類推。",
10191019 "Prompt": "提示詞",
10201020 "The prompt to be sent.": "要發送的提示詞。",
10211021 "This prompt cannot be overridden by character cards, even if overrides are preferred.": "即使啟用偏好覆寫,此提示詞也不能被角色卡片覆寫。",
@@ -1050,32 +1050,32 @@
10501050 "welcome_message_part_6": "加入",
10511051 "Discord server": "不和諧伺服器",
10521052 "welcome_message_part_7": "取得公告和資訊。",
10531053 "SillyTavern is aimed at advanced users.": "SillyTavern 旨在進階使用者專為進階使用者設計",
10541054 "If you're new to this, enable the simplified UI mode below.": "如果您是新手,請啟用下方的簡化請啟用下方的簡易 UI 模式",
10551055 "Change it later in the 'User Settings' panel.": "稍後可在「使用者設定」面板中變更",
10561056 "Enable simple UI mode": "啟用簡單啟用簡易 UI 模式",
10571057 "Looking for AI characters?": "正在尋找 AI 角色人物角色?",
10581058 "onboarding_import": "匯入",
10591059 "from supported sources or view": "從支援的來源或檢視",
10601060 "Sample characters": "範例角色人物範例角色",
10611061 "Your Persona": "您的玩家角色人物您的使用者角色",
10621062 "Before you get started, you must select a persona name.": "在開始之前,您必須選擇一個玩家角色人物名稱您必須選擇一個使用者角色名稱",
10631063 "welcome_message_part_8": "這個隨時可以透過",
10641064 "welcome_message_part_9": "圖示來變更。",
10651065 "Persona Name:": "玩家角色人物名稱使用者角色名稱",
10661066 "Temporarily disable automatic replies from this character": "暫時停用此角色人物的自動回覆暫時停用此角色的自動回覆",
10671067 "Enable automatic replies from this character": "啟用此角色人物的自動回覆啟用此角色的自動回覆",
10681068 "Trigger a message from this character": "觸發此角色人物的訊息觸發此角色的訊息",
10691069 "Move up": "上移",
10701070 "Move down": "下移",
10711071 "View character card": "查看角色人物卡查看角色卡",
10721072 "Remove from group": "從群組中移除",
10731073 "Add to group": "新增到群組",
10741074 "Alternate Greetings": "額外問候語",
10751075 "Alternate_Greetings_desc": "這些將在開始新聊天時顯示為第一條訊息的滑動選項這些將在開始新聊天時顯示為第一則訊息的滑動選項。\n群組成員可以選擇其中之一來開始對話。",
10761076 "Alternate Greetings Hint": "額外問候語的提示訊息",
10771077 "(This will be the first message from the character that starts every chat)": "(這將是每次聊天開始時角色人物發送的第一條訊息)(這將是每次聊天開始時角色發送的第一則訊息)",
10781078 "Forbid Media Override explanation": "當前角色/此角色/群組在聊天中使用外部媒體的能力。",
10791079 "Forbid Media Override subtitle": "禁止媒體覆寫副標題",
10801080 "Always forbidden": "總是禁止",
10811081 "Always allowed": "總是允許",
@@ -1084,16 +1084,16 @@
10841084 "Unique to this chat": "此聊天獨有",
10851085 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "檢查點繼承其上級的註釋,之後可以單獨更改",
10861086 "Include in World Info Scanning": "包括在世界資訊掃描中",
10871087 "Before Main Prompt / Story String": "在主要提示詞 / 故事字串之前",
10881088 "After Main Prompt / Story String": "在主要提示詞 / 故事字串之後",
10891089 "as": "作為",
10901090 "Insertion Frequency": "插入頻率",
10911091 "(0 = Disable, 1 = Always)": "(0 = 停用, 1 = 永久)",
10921092 "User inputs until next insertion:": "使用者輸入直到下一次插入:",
10931093 "Character Author's Note (Private)": "角色人物作者備註(角色作者備註(私人)",
10941094 "Won't be shared with the character card on export.": "不會在匯出時與角色人物卡共享不會在匯出時與角色卡共享",
10951095 "Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "將自動新增為此角色人物的作者備註將自動新增為此角色的作者備註。將在群組中使用,但在群組聊天開啟時無法修改",
10961096 "Use character author's note": "使用角色人物作者備註使用角色作者備註",
10971097 "Replace Author's Note": "取代作者註釋",
10981098 "Default Author's Note": "預設作者註釋",
10991099 "Will be automatically added as the Author's Note for all new chats.": "將自動新增為所有新聊天的作者備註",
@@ -1101,24 +1101,24 @@
11011101 "1 = disabled": "1 = 停用",
11021102 "write short replies, write replies using past tense": "撰寫簡短回覆,使用過去式撰寫回覆",
11031103 "Positive Prompt": "正面提示詞",
11041104 "Use character CFG scales": "使用角色人物使用角色 CFG 比例",
11051105 "Character CFG": "角色人物角色 CFG",
11061106 "Will be automatically added as the CFG for this character.": "將自動新增為此角色人物的將自動新增為此角色的 CFG",
11071107 "Global CFG": "全域 CFG",
11081108 "Will be used as the default CFG options for every chat unless overridden.": "將作為每次聊天的預設 CFG 選項,除非被覆寫",
11091109 "CFG Prompt Cascading": "CFG 提示詞級聯",
11101110 "Combine positive/negative prompts from other boxes.": "結合其他文字方塊中的正/負提示詞",
11111111 "For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "例如勾選聊天、全域和角色人物框將所有負面提示詞合併為一個逗號分隔的字串全域和角色框將所有負面提示詞合併為一個逗號分隔的字串",
11121112 "Always Include": "總是包含",
11131113 "Chat Negatives": "聊天負面提示詞",
11141114 "Character Negatives": "角色人物負面提示詞角色負面提示詞",
11151115 "Global Negatives": "全域負面提示詞",
11161116 "Custom Separator:": "自訂分隔符:",
11171117 "Insertion Depth:": "插入深度:",
11181118 "Token Probabilities": "符記機率符元機率",
11191119 "Select a token to see alternatives considered by the AI.": "選擇一個符記以檢視選擇一個符元以檢視 AI 考慮的替代方案",
11201120 "Not connected to API!": "未連線到 API!",
11211121 "Type a message, or /? for help": "輸入一個訊息,輸入一則訊息,或輸入 /? 取得支援",
11221122 "Continue script execution": "繼續腳本執行",
11231123 "Pause script execution": "暫停腳本執行",
11241124 "Abort script execution": "中止腳本執行",
@@ -1138,13 +1138,13 @@
11381138 "Impersonate": "AI 扮演使用者",
11391139 "Continue": "繼續",
11401140 "Bind user name to that avatar": "將使用者名稱附加到該頭像",
11411141 "Change persona image": "更改玩家角色人物圖片更改使用者角色圖片",
11421142 "Select this as default persona for the new chats.": "選擇此作為新聊天的預設玩家角色人物選擇此作為新聊天的預設使用者角色。",
11431143 "Delete persona": "刪除玩家角色人物刪除使用者角色",
11441144 "These characters are the winners of character design contests and have outstandable quality.": "這些角色都是角色設計比賽的優勝者,品質卓越。",
11451145 "Contest Winners": "比賽獲勝者",
11461146 "These characters are the finalists of character design contests and have remarkable quality.": "這些角色都是角色設計比賽的入圍作品,品質卓越。",
11471147 "Featured Characters": "特色角色人物特色角色",
11481148 "Attach a File": "附加檔案",
11491149 "Open Data Bank": "打開資料儲藏庫",
11501150 "Enter a URL or the ID of a Fandom wiki page to scrape:": "輸入 URL 或 Fandom 維基頁面的 ID 來抓取:",
@@ -1155,42 +1155,42 @@
11551155 "File per article": "每篇文章一個檔案",
11561156 "Each article will be saved as a separate file.": "每篇文章將另存為一個檔案。",
11571157 "Data Bank": "資料儲藏庫",
11581158 "These files will be available for extensions that support attachments (e.g. Vector Storage).": "這些檔案將可用於支援附件的擴充套件(這些檔案將可用於支援附件的擴充功能(例如向量存儲)。",
11591159 "Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.": "支援的檔案類型:純文字,PDF,Markdown,HTML,EPUB。",
11601160 "Drag and drop files here to upload.": "拖放檔案到這裡以上傳。",
11611161 "Date (Newest First)": "日期(最新優先)",
11621162 "Date (Oldest First)": "日期(最舊優先)",
11631163 "Name (A-Z)": "名稱(A-Z)",
11641164 "Name (Z-A)": "名稱(Z-A)",
11651165 "Size (Smallest First)": "尺寸(由小到大)",
11661166 "Size (Largest First)": "尺寸(由大到小)",
11671167 "Bulk Edit": "批次編輯",
11681168 "Select All": "全選",
11691169 "Select None": "選擇無",
11701170 "Global Attachments": "全域附件",
11711171 "These files are available for all characters in all chats.": "這些檔案在所有聊天中的所有角色人物中可用這些檔案在所有聊天中的所有角色中可用。",
11721172 "Character Attachments": "角色人物附件角色附件",
11731173 "These files are available the current character in all chats they are in.": "這些檔案在當前角色人物參與的所有聊天中可用這些檔案在此角色參與的所有聊天中可用。",
11741174 "Saved locally. Not exported.": "本機儲存。不匯出。",
11751175 "Chat Attachments": "聊天附件",
11761176 "These files are available to all characters in the current chat.": "這些檔案在當前聊天中的所有角色人物中可用這些檔案在此聊天中的所有角色中可用。",
11771177 "Enter a base URL of the MediaWiki to scrape.": "輸入要抓取的 MediaWiki 的基礎 URL。",
11781178 "Don't include the page name!": "不要包括頁面名稱!",
11791179 "Enter web URLs to scrape (one per line):": "輸入要抓取的網頁 URL(每行一個):",
11801180 "Enter a video URL to download its transcript.": "輸入影片 URL 來下載其文字記錄。",
11811181 "Expression API": "表達 API",
11821182 "Fallback Expression": "回退表達式",
11831183 "ext_sum_with": "總結一下:",
11841184 "ext_sum_main_api": "主要API",
11851185 "ext_sum_current_summary": "目前摘要:",
11861186 "ext_sum_restore_previous": "恢復上一個還原上一個",
11871187 "ext_sum_memory_placeholder": "摘要將在此處產生...將在此產生摘要⋯",
11881188 "Trigger a summary update right now.": "立即觸發摘要更新。",
11891189 "ext_sum_force_text": "現在總結一下",
11901190 "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "停用自動摘要更新。暫停時,摘要保持原樣。您仍然可以透過按下「立即匯總」按鈕強制更新(僅適用於 Main API)。",
11911191 "ext_sum_pause": "暫停",
11921192 "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "從要總結的文本中省略世界資訊和作者備註。僅在使用主要 API 時有效。額外 API 總是省略 WI/AN總是省略世界資訊和作者備註。",
11931193 "ext_sum_no_wi_an": "無無線網路/無線網路無法連接到網際網路",
11941194 "ext_sum_settings_tip": "編輯摘要提示、插入位置等",
11951195 "ext_sum_settings": "摘要設定",
11961196 "ext_sum_prompt_builder": "提示產生器",
@@ -1198,17 +1198,17 @@
11981198 "ext_sum_prompt_builder_1": "原始、阻塞",
11991199 "ext_sum_prompt_builder_2_desc": "擴充功能將使用尚未匯總的訊息建立自己的提示。產生摘要時不會阻止聊天。並非所有後端都支援此模式。",
12001200 "ext_sum_prompt_builder_2": "原始、非阻塞",
12011201 "ext_sum_prompt_builder_3_desc": "擴充功能將使用常規主提示產生器並將摘要請求新增至其中作為最後一個系統訊息擴充功能將使用常規主提示產生器並將摘要請求新增至其中作為最後一則系統訊息。",
12021202 "ext_sum_prompt_builder_3": "經典,阻塞",
12031203 "Summary Prompt": "摘要提示詞",
12041204 "ext_sum_restore_default_prompt_tip": "恢復預設提示還原預設提示",
12051205 "ext_sum_prompt_placeholder": "這個提示詞將發送給 AI 以請求生成摘要。{{words}} 將解析為「字數」參數。",
12061206 "ext_sum_target_length_1": "目標摘要長度",
12071207 "ext_sum_target_length_2": "(",
12081208 "ext_sum_target_length_3": "字)",
12091209 "ext_sum_api_response_length_1": "API響應長度API 響應長度",
12101210 "ext_sum_api_response_length_2": "(",
12111211 "ext_sum_api_response_length_3": "代幣個符元)",
12121212 "ext_sum_0_default": "0 = 預設",
12131213 "ext_sum_raw_max_msg": "[原始] 每個請求的最大訊息數",
12141214 "ext_sum_0_unlimited": "0 = 無限制",
@@ -1225,7 +1225,7 @@
12251225 "ext_sum_injection_position": "注射位置",
12261226 "How many messages before the current end of the chat.": "在聊天目前結束前有多少訊息。",
12271227 "ext_regex_title": "正規表示式",
12281228 "ext_regex_new_global_script": "+ 全球全域",
12291229 "ext_regex_new_scoped_script": "+ 範圍",
12301230 "ext_regex_import_script": "匯入腳本",
12311231 "ext_regex_global_scripts": "全域腳本",
@@ -1234,9 +1234,9 @@
12341234 "ext_regex_scoped_scripts_desc": "僅適用於此角色。儲存到卡片資料中。",
12351235 "Regex Editor": "正規表示式編輯器",
12361236 "Test Mode": "測試模式",
12371237 "ext_regex_desc": "正規表示式(Regex)是一種使用正規表示式尋找/取代字串的工具。如果您想了解更多,請點選標題旁邊的()",
12381238 "Input": "輸入",
12391239 "ext_regex_test_input_placeholder": "在此輸入...",
12401240 "Output": "輸出",
12411241 "ext_regex_output_placeholder": "空的",
12421242 "Script Name": "腳本名稱",
@@ -1256,40 +1256,40 @@
12561256 "ext_regex_other_options": "其他選項",
12571257 "Only Format Display": "僅格式化顯示",
12581258 "ext_regex_only_format_prompt_desc": "聊天記錄不會更改,只會更改發送請求時的提示(生成時)。",
12591259 "Only Format Prompt (?)": "僅格式化提示詞()",
12601260 "Run On Edit": "編輯時執行",
12611261 "ext_regex_substitute_regex_desc": "在執行「尋找正規表示式」前取代 {{macros}}",
12621262 "Substitute Regex": "取代正規表示式",
12631263 "ext_regex_import_target": "導入至匯入至:",
12641264 "ext_regex_disable_script": "停用腳本",
12651265 "ext_regex_enable_script": "啟用腳本",
12661266 "ext_regex_edit_script": "編輯腳本",
12671267 "ext_regex_move_to_global": "移至全域腳本",
12681268 "ext_regex_move_to_scoped": "移至作用域腳本",
12691269 "ext_regex_export_script": "導出腳本匯出腳本",
12701270 "ext_regex_delete_script": "刪除腳本",
12711271 "Trigger Stable Diffusion": "觸發 Stable Diffusion",
12721272 "sd_Yourself": "角色人物角色",
12731273 "sd_Your_Face": "角色人物的臉角色的臉",
12741274 "sd_Me": "玩家使用者",
12751275 "sd_The_Whole_Story": "整篇故事",
12761276 "sd_The_Last_Message": "最後一個訊息最後一則訊息",
12771277 "sd_Raw_Last_Message": "最後一個原始訊息最後一則原始訊息",
12781278 "sd_Background": "背景",
12791279 "Image Generation": "圖片生成",
12801280 "sd_refine_mode": "允許在傳送至生成 API 前,手動編輯提示詞字串",
12811281 "sd_refine_mode_txt": "生成前編輯提示詞",
12821282 "sd_interactive_mode": "當發送「給我一張貓的圖片」這類訊息時,自動生成圖片。",
12831283 "sd_interactive_mode_txt": "互動模式",
12841284 "sd_multimodal_captioning": "根據使用者和角色人物的頭像根據使用者和角色的頭像,使用多模態模型描述生成肖像提示詞。",
12851285 "sd_multimodal_captioning_txt": "對肖像使用多模態模型描述",
12861286 "sd_expand": "使用文字生成模型自動擴充提示詞。",
12871287 "sd_expand_txt": "自動擴充提示詞",
12881288 "sd_snap": "對於具有特定長寬比的生成請求(如肖像、背景),將其調整至最接近的已知解析度,同時儘量保持絕對像素數(建議用於SDXL)(建議用於 SDXL)。",
12891289 "sd_snap_txt": "自動調整解析度",
12901290 "Source": "來源",
12911291 "sd_auto_url": "範例: {{auto_url}}",
12921292 "Authentication (optional)": "授權驗證(選填)",
12931293 "Example: username:password": "範例:帳號:密碼",
12941294 "Important:": "重要:",
12951295 "sd_auto_auth_warning_1": "使用",
@@ -1300,7 +1300,7 @@
13001300 "The server must be accessible from the SillyTavern host machine.": "伺服器必須能夠被 SillyTavern 主機存取。",
13011301 "Hint: Save an API key in Horde KoboldAI API settings to use it here.": "提示訊息:在 Horde KoboldAI API 設定中儲存一個 API 金鑰,以便在此使用。",
13021302 "Allow NSFW images from Horde": "允許來自 Horde 的 NSFW 圖片",
13031303 "Sanitize prompts (recommended)": "清理提示詞(建議)",
13041304 "Automatically adjust generation parameters to ensure free image generations.": "自動調整生成參數以確保生成免費的圖片。",
13051305 "Avoid spending Anlas": "避免消耗 Anlas 點數",
13061306 "Opus tier": "Opus 級別",
@@ -1318,12 +1318,12 @@
13181318 "Refine": "精煉",
13191319 "Decrisper": "德克里斯珀",
13201320 "Sampling steps": "取樣步數",
13211321 "Width": "寬度(W)(Width)",
13221322 "Height": "高度(H)(Height)",
13231323 "Resolution": "解析度",
13241324 "Model": "模型",
13251325 "Sampling method": "取樣方法",
13261326 "Karras (not all samplers supported)": "Karras(並非所有取樣器都支援)(並非所有取樣器均受支援)",
13271327 "SMEA versions of samplers are modified to perform better at high resolution.": "SMEA 版本的取樣器經過修改,能在高解析度下表現更佳。",
13281328 "SMEA": "SMEA",
13291329 "DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA 取樣器的 DYN 變體通常會產生更多樣化的輸出,但在非常高的解析度下可能會失敗。",
@@ -1334,7 +1334,7 @@
13341334 "Upscaler": "放大演算法",
13351335 "Upscale by": "放大倍率",
13361336 "Denoising strength": "重繪幅度",
13371337 "Hires steps (2nd pass)": "高解析步驟 (2nd pass)",
13381338 "Preset for prompt prefix and negative prompt": "提示詞前綴和負面提示詞的預設設定",
13391339 "Style": "樣式",
13401340 "Save style": "儲存樣式",
@@ -1342,11 +1342,11 @@
13421342 "Common prompt prefix": "通用提示詞前綴",
13431343 "sd_prompt_prefix_placeholder": "使用 {prompt} 指定生成的提示詞將被插入的位置。",
13441344 "Negative common prompt prefix": "負面通用提示詞前綴",
13451345 "Character-specific prompt prefix": "角色人物特定提示詞前綴角色特定提示詞前綴",
13461346 "Won't be used in groups.": "不會用在群組",
13471347 "sd_character_prompt_placeholder": "描述當前選擇角色人物的特徵描述該選擇角色的特徵。這些特徵將增加在通用提示詞前綴之後。\n例如:女性、綠色眼睛、棕色頭髮、粉紅色襯衫。",
13481348 "Character-specific negative prompt prefix": "角色人物特定負面提示詞前綴角色特定負面提示詞前綴",
13491349 "sd_character_negative_prompt_placeholder": "不應出現在選定角色人物上的任何特徵不應出現在選定角色上的任何特徵。這些特徵將增加在負面通用提示詞前綴之後。例如:珠寶、鞋子、眼鏡。",
13501350 "Shareable": "可分享",
13511351 "Image Prompt Templates": "圖片提示詞範本",
13521352 "Vectors Model Warning": "向量模型警告",
@@ -1359,7 +1359,7 @@
13591359 "User Handle:": "使用者控制代碼:",
13601360 "Password:": "密碼:",
13611361 "Confirm Password:": "確認密碼:",
13621362 "This will create a new subfolder...": "這將建立一個新的子資料夾...",
13631363 "Current Password:": "目前密碼:",
13641364 "New Password:": "新密碼:",
13651365 "Confirm New Password:": "確認新密碼:",
@@ -1371,27 +1371,27 @@
13711371 "Warning:": "警告:",
13721372 "This action is irreversible.": "此動作不可逆轉。",
13731373 "Type the user's handle below to confirm:": "在下方輸入使用者的控制代碼以確認:",
13741374 "Import Characters": "導入角色匯入角色",
13751375 "Enter the URL of the content to import": "輸入要匯入的內容的 URL",
13761376 "Supported sources:": "支持的來源:",
13771377 "char_import_1": "Chub角色(直接連結或ID直接連結或 ID)",
13781378 "char_import_example": "例子:",
13791379 "char_import_2": "Chub Lorebook(直接連結或ID直接連結或 ID)",
13801380 "char_import_3": "JanitorAI 角色(直接連結或 UUIDID)",
13811381 "char_import_4": "Pygmalion.chat 角色(直接連結或 UUIDID)",
13821382 "char_import_5": "AICharacterCard.com 角色(直接連結或 ID)",
13831383 "char_import_6": "直接 PNG 連結(請參閱",
13841384 "char_import_7": "對於允許的主機)",
13851385 "char_import_8": "RisuRealm角色(直接連結)",
13861386 "Supports importing multiple characters.": "支援導入多個字元支援匯入多個字元。",
13871387 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
13881388 "Export for character": "匯出字符",
13891389 "Export prompts for this character, including their order.": "匯出該角色的提示,包括其順序。",
13901390 "Export all": "全部導出全部匯出",
13911391 "Export all your prompts to a file": "將所有提示匯出到文件",
13921392 "Insert prompt": "插入提示",
13931393 "Delete prompt": "刪除提示",
13941394 "Import a prompt list": "導入提示列表匯入提示列表",
13951395 "Export this prompt list": "匯出此提示列表",
13961396 "Reset current character": "重置目前字符",
13971397 "New prompt": "新提示",
@@ -1403,12 +1403,12 @@
14031403 "Settings Snapshots": "設定值快照",
14041404 "Record a snapshot of your current settings.": "記錄目前設定的快照。",
14051405 "Make a Snapshot": "建立快照",
14061406 "Restore this snapshot": "恢復此快照還原此快照",
14071407 "Hi,": "嗨,",
14081408 "To enable multi-account features, restart the SillyTavern server with": "要啟用多帳號功能,請在 config.yaml 文件中將",
14091409 "set to true in the config.yaml file.": "設為 true,然後重啟 後重啟 SillyTavern 伺服器。",
14101410 "Account Info": "帳號資訊",
14111411 "To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "要更改您的使用者頭像,請使用下方按鈕或在玩家角色管理選單中選擇一個預設人物請使用下方按鈕或在使用者角色管理選單中選擇一個預設人物。",
14121412 "Set your custom avatar.": "設定您的頭像。",
14131413 "Remove your custom avatar.": "移除您的頭像。",
14141414 "Handle:": "使用者名稱:",
@@ -1428,12 +1428,12 @@
14281428 "Want to update?": "想要更新嗎?",
14291429 "How to start chatting?": "如何開始聊天?",
14301430 "Click _space": "點選",
14311431 "and select a": "並選擇一個並擇一",
14321432 "Chat API": "聊天 API",
14331433 "and pick a character.": "並選擇一個角色人物並選擇一個角色。",
14341434 "You can browse a list of bundled characters in the": "您可以在",
14351435 "Download Extensions & Assets": "下載擴充套件和資產下載擴充功能和資產",
14361436 "menu within": "選單中瀏覽內建角色人物列表選單中瀏覽內建角色列表",
14371437 "Confused or lost?": "困惑或迷路了嗎?",
14381438 "click these icons!": "點選這些圖示!",
14391439 "in the chat bar": "到聊天欄中",
@@ -1442,5 +1442,350 @@
14421442 "Still have questions?": "還有問題嗎?",
14431443 "Join the SillyTavern Discord": "加入 SillyTavern Discord",
14441444 "Post a GitHub issue": "發布 GitHub 問題",
14451445 "Contact the developers": "聯繫開發者",
1446+ "(-1 for random)": "(-1 表示隨機)",
1447+ "(Optional)": "(可選)",
1448+ "(use _space": "(使用",
1449+ "[no_connection_text]api_no_connection": "未連線⋯",
1450+ "[no_desc_text]No model description": "[無描述]",
1451+ "[no_items_text]openai_logit_bias_no_items": "無項目",
1452+ "[placeholder]Any contents here will replace the default Post-History Instructions used for this character. (v2 specpost_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 specpost_history_instructions)",
1453+ "[placeholder]comma delimited,no spaces between": "逗號分割,無需空格",
1454+ "[placeholder]e.g. black-forest-labs/FLUX.1-dev": "例如:black-forest-labs/FLUX.1-dev",
1455+ "[placeholder]Example: gpt-3.5-turbo": "例如:gpt-3.5-turbo",
1456+ "[placeholder]Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
1457+ "[popup-button-crop]popup-button-crop": "裁剪",
1458+ "[title](disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)",
1459+ "[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)",
1460+ "[title]A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。",
1461+ "[title]A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用區域擴展的倍數。",
1462+ "[title]Abort current image generation task": "終止目前的圖片生成任務",
1463+ "[title]Add Character and User names to a list of stopping strings.": "將角色和使用者角色名稱添加至停止字符列表。",
1464+ "[title]Alignment for rank nodes.": "對排名節點的對齊方式。",
1465+ "[title]Always show the node full info panel at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的完整資訊面板顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。",
1466+ "[title]Always show the node tooltip at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的工具提示欄顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。",
1467+ "[title]Apply current sorting as Order": "應用此排序為順序",
1468+ "[title]Cap the number of entry activation recursions": "限制入口啟動的遞歸次數",
1469+ "[title]Caption": "標題",
1470+ "[title]Close popup": "關閉彈出視窗",
1471+ "[title]Color configuration for Timelines when 'Use UI Theme' in Style Settings is off.": "關閉「使用介面主題」的時間線顏色。",
1472+ "[title]context_allow_post_history_instructions": "在文本完成模式中包含聊天歷史後指示(Post-History Instructions),但可能導致不良輸出。",
1473+ "[title]Create a new connection profile": "建立新的連線設定檔",
1474+ "[title]Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定義導入卡片時應採取的動作。選擇「詢問」將始終顯示對話框。",
1475+ "[title]delay_until_recursion_level": "定義遞迴掃描的延遲層級。\r最初僅匹配第一層(數字最小的層級)。\r未找到匹配時,下一層將成為可匹配的層級。\r此過程會重複,直到所有層級都被檢查完畢。\r與「延遲至遞歸」設定相關聯。",
1476+ "[title]Delete a connection profile": "刪除連線設定檔",
1477+ "[title]Delete template": "刪除模板",
1478+ "[title]Delete the template": "刪除此模板",
1479+ "[title]Disabling is not recommended.": "不建議禁用。",
1480+ "[title]Display swipe numbers for all messages, not just the last.": "顯示所有訊息的滑動編號,而不僅是最後一條訊息。",
1481+ "[title]Duplicate persona": "複製使用者角色",
1482+ "[title]Edit a connection profile": "編輯連線設定檔",
1483+ "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "啟用自動選擇輸入框中的文本,適用於彈出輸入框及其他自定義輸入框。",
1484+ "[title]Entries with a cooldown can't be activated N messages after being triggered.": "設有冷卻時間的條目於觸發後的 N 條訊息內無法再次啟用。",
1485+ "[title]Entries with a delay can't be activated until there are N messages present in the chat.": "有延遲的條目需等待聊天中出現 N 條訊息後才能啟用。",
1486+ "[title]Expand swipe nodes when the timeline view first opens, and whenever the graph is refreshed. When off, you can expand them by long-pressing a node, or by pressing the Toggle Swipes button.": "時間線視圖首次打開或刷新時展開滑動節點。關閉時可通過長按節點或點選「切換滑動」按鈕展開。",
1487+ "[title]Export Advanced Formatting settings": "匯出高級格式設定",
1488+ "[title]Export template": "匯出模板",
1489+ "[title]Find similar characters": "尋找相似角色",
1490+ "[title]Height of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時的節點像素高度。",
1491+ "[title]How the automatic graph builder assigns a rank (layout depth) to graph nodes.": "自動圖表生成器分配圖節點等級(佈局深度)的方式。",
1492+ "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "勾選後,將使用角色卡中的聊天歷史後指示(Post-History Instructions)覆蓋。",
1493+ "[title]Import Advanced Formatting settings": "匯入進階格式設定\n\n您也可以提供舊版檔案作為提示詞和上下文範本使用。",
1494+ "[title]Import template": "匯入模板",
1495+ "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群組聊天中,突出顯示該生成回應的角色及順序。",
1496+ "[title]Include names with each message into the context for scanning": "將每條訊息的名稱納入掃描上下文",
1497+ "[title]Inserted before the first User's message": "插入到第一條使用者訊息前。",
1498+ "[title]instruct_enabled": "啟用指令模式(Instruct Mode)",
1499+ "[title]instruct_last_input_sequence": "插入到最後一條使用者訊息之前。",
1500+ "[title]instruct_template_activation_regex_desc": "連接 API 或選擇模型時,若模型名稱符合所提供的正規表達式,則自動啟動該指令模板(Instruct Template)。",
1501+ "[title]Load Asset List": "載入資源列表",
1502+ "[title]load_asset_list_desc": "根據資源列表文件載入擴展及資源。\n\n該字段中的默認資源 URL 指向官方的擴展及資源的列表。\n可在此插入您的自定義資源列表。\n\n若需安裝單個第三方擴展,請使用右上角的「安裝擴展」按鈕。",
1503+ "[title]markdown_hotkeys_desc": "啟用熱鍵以在某些文本輸入框中插入 Markdown 格式字符。詳見「/help hotkeys」。",
1504+ "[title]Not all samplers supported.": "並非所有採樣器均受支援。",
1505+ "[title]Open the timeline view. Same as the slash command '/tl'.": "打開時間線視圖,與斜杠指令「/tl」相同。",
1506+ "[title]Penalize sequences based on their length.": "根據序列長度進行懲罰。",
1507+ "[title]Reload a connection profile": "重新載入連線設定檔",
1508+ "[title]Rename current preset": "重新命名此預設",
1509+ "[title]Rename current prompt": "重新命名此提示詞",
1510+ "[title]Rename current template": "重新密名此模板",
1511+ "[title]Reset all Timelines settings to their default values.": "將所有時間軸設定重置為預設值。",
1512+ "[title]Restore current prompt": "還原目前的提示詞",
1513+ "[title]Restore current template": "還原目前的模板",
1514+ "[title]Save prompt as": "另存提示詞為",
1515+ "[title]Save template as": "另存模板為",
1516+ "[title]sd_adetailer_face": "在生成過程中使用 ADetailer 臉部模型。需在後端安裝 ADetailer 擴展。",
1517+ "[title]sd_free_extend": "自動使用目前選定的 LLM 擴展自由模式主題提示詞(不包括肖像或背景)。",
1518+ "[title]sd_function_tool": "使用功能工具自動檢測意圖以生成圖片。",
1519+ "[title]Seed_desc": "用於生成確定性和可重現輸出的隨機種子。設定為 -1 時將使用隨機種子。",
1520+ "[title]Select your current Context Template": "選擇您目前的上下文模板",
1521+ "[title]Select your current Instruct Template": "選擇您目前的指令模板",
1522+ "[title]Select your current System Prompt": "選擇您目前的系統提示詞",
1523+ "[title]Separation between adjacent edges in the same rank.": "同一層級中相鄰邊之間的間距。",
1524+ "[title]Separation between adjacent nodes in the same rank.": "同一層級中相鄰節點之間的間距。",
1525+ "[title]Separation between each rank in the layout.": "佈局中各層級之間的間距。",
1526+ "[title]Settings for the visual appearance of the Timelines graph.": "時間線圖形的視覺外觀設置。",
1527+ "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "在輸入框中添加按鈕,讓 AI 模仿您的角色身份發送一則訊息。",
1528+ "[title]Show a legend for colors corresponding to different characters and chat checkpoints.": "顯示一個圖例,標註不同角色和對話檢查點對應的顏色。",
1529+ "[title]Show the AI character's avatar as the graph root node. When off, the root node is blank.": "將 AI 角色的頭像作為圖形的根節點;關閉時,根節點為空。",
1530+ "[title]Sticky entries will stay active for N messages after being triggered.": "觸發後,置頂條目將在接下來的 N 條訊息中保持活躍。",
1531+ "[title]stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} 和 {{getglobalvar::}} 巨集的字面巨集樣值被自動解析。\n例如,{{newline}} 將保持為字面字串 {{newline}}。\n\n(此功能通過內部將 {{getvar::}} 和 {{getglobalvar::}} 巨集替換為具範圍的變數來實現。)",
1532+ "[title]Style and routing of graph edges.": "圖形邊的樣式和路徑。",
1533+ "[title]Swap width and height": "交換寬度與高度",
1534+ "[title]Swipe left": "向左滑動",
1535+ "[title]Swipe right": "向右滑動",
1536+ "[title]Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry": "切換角色/標籤篩選,將列出的角色和標籤從此條目的匹配中排除",
1537+ "[title]sysprompt_enabled": "啟用系統提示詞",
1538+ "[title]The number of sequences generated at each step with Beam Search.": "在 Beam Search 中每一步生成的序列數量。",
1539+ "[title]The visual appearance of a node in the graph.": "圖形中節點的視覺外觀。",
1540+ "[title]Update a connection profile": "更新連線設定檔",
1541+ "[title]Update current prompt": "更新此提示詞",
1542+ "[title]Update current template": "更新此模板",
1543+ "[title]Use GPU acceleration for positioning the full info panel that appears when you click a node. If the tooltip arrow tends to disappear, turning this off may help.": "啟用 GPU 加速來定位點擊節點時出現的完整資訊面板。若發現工具提示箭頭經常消失,可考慮關閉此功能。",
1544+ "[title]Use the colors of the ST GUI theme, instead of the colors configured in Color Settings specifically for this extension.": "以 ST GUI 主題的顏色,取代「顏色設定」中針對此擴展特別配置的顏色。",
1545+ "[title]View connection profile details": "查看連線設定檔詳情",
1546+ "[title]When enabled, nodes that have swipes splitting off of them will appear subtly larger, in addition to having the double border.": "啟用後,具分支滑動的節點將顯示雙重邊框,還會略微放大。",
1547+ "[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "世界資訊條目狀態:\\r🔵 恆定\\r🟢 正常\\r🔗 向量化",
1548+ "[title]Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。",
1549+ "[title]world_button_title": "角色背景設定\\n\\n點擊以載入\\nShift+ 點擊開啟「連結至世界資訊」彈窗",
1550+ "# of Beams": "# of Beams",
1551+ "01.AI API Key": "01.AI API 金鑰",
1552+ "01.AI Model": "01.AI 模型",
1553+ "取消": "取消",
1554+ "Additional Parameters": "其他參數",
1555+ "All": "全部",
1556+ "Allow fallback models": "允許回退模型",
1557+ "Allow fallback providers": "允許回退供應商",
1558+ "Allow Post-History Instructions": "允許聊天歷史後指示",
1559+ "Allow reverse proxy": "允許反向代理",
1560+ "Alternate Greeting #": "備選問候語 #",
1561+ "alternate_greetings_hint_1": "點擊",
1562+ "alternate_greetings_hint_2": "按鈕開始!",
1563+ "Always": "總是",
1564+ "ANY support requests will be REFUSED if you are using a proxy.": "使用代理時,所有支援請求均不予受理。",
1565+ "API": "API",
1566+ "API Key": "API 金鑰",
1567+ "Ask": "詢問",
1568+ "Ask every time": "每次都詢問",
1569+ "Assets URL": "資源 URL",
1570+ "Assistant Message Sequences": "助理訊息序列",
1571+ "Assistant Prefix": "助理訊息前綴",
1572+ "Assistant Suffix": "助理訊息後綴",
1573+ "at the end of the URL!": "在 URL 末尾!",
1574+ "Audio Playback Speed": "音檔播放速度",
1575+ "Auto-select Input Text": "自動選擇輸入文本",
1576+ "Automatically caption images": "自動為圖片添加字幕",
1577+ "Auxiliary": "輔助",
1578+ "Background Image": "背景圖片",
1579+ "Block Entropy API Key": "Block Entropy API 金鑰",
1580+ "Can be set manually or with an _space": "可以手動設置或使用 _space",
1581+ "Caption Prompt": "字幕提示詞",
1582+ "category": "類別",
1583+ "Character Expressions": "角色情緒立繪",
1584+ "Character Node Color": "角色節點顏色",
1585+ "character_names_none": "避免使用角色名稱作為前綴。此功能在群組中可能表現不理想,需謹慎考慮。",
1586+ "Characters": "角色",
1587+ "Chat Message Visibility (by source)": "聊天訊息可見性(按來源)",
1588+ "Chat vectorization settings": "聊天向量化設定",
1589+ "Checked: all entries except ❌ status can be activated.": "勾選:除 ❌ 狀態外的所有條目均可啟動。",
1590+ "Checkpoint Color": "檢查點節點邊框顏色",
1591+ "Chunk boundary": "Chunk 邊界",
1592+ "Chunk overlap (%)": "Chunk 重疊(%)",
1593+ "Chunk size (chars)": "Chunk 大小(字符數)",
1594+ "class": "所有類別",
1595+ "Classifier API": "分類器 API",
1596+ "Click to set": "點擊以設定",
1597+ "CLIP Skip": "CLIP 跳過",
1598+ "Completion Object": "完成對象",
1599+ "Conf": "設定檔",
1600+ "Connection Profile": "連線設定檔",
1601+ "Cooldown": "冷卻時間",
1602+ "Create new folder in the _space": "在 _space 中創建新文件夾",
1603+ "currently_loaded": "[目前已載入]",
1604+ "currently_selected": "[目前已選取]",
1605+ "Custom (OpenAI-compatible)": "自定義(相容 OpenAI)",
1606+ "Custom Expressions": "自定義情緒圖像",
1607+ "Data Bank files": "數據庫文件",
1608+ "Default / Fallback Expression": "默認/回退表情",
1609+ "Delay": "延遲",
1610+ "Delay until recursion (can only be activated on recursive checking)": "遞迴掃描延遲(僅在啟用遞迴掃描時可用)",
1611+ "Do not proceed if you do not agree to this!": "若不同意此條款,請勿繼續!",
1612+ "Edge Color": "邊緣顏色",
1613+ "Edit captions before saving": "在保存前編輯字幕",
1614+ "Enable for files": "為文件啟用",
1615+ "Enable for World Info": "為世界資訊啟用",
1616+ "enable_functions_desc_1": "允許使用",
1617+ "enable_functions_desc_2": "功能工具",
1618+ "enable_functions_desc_3": "可供多種擴展利用,實現更多功能。",
1619+ "Enabled for all entries": "對所有條目啟用",
1620+ "Enabled for chat messages": "對聊天訊息啟用",
1621+ "Endpoint URL": "端點 URL",
1622+ "Enter a Model ID": "輸入模型 ID",
1623+ "Example: https://****.endpoints.huggingface.cloud": "例如:https://****.endpoints.huggingface.cloud",
1624+ "Exclude": "排除",
1625+ "Exclude Top Choices (XTC)": "排除頂部選項(XTC)",
1626+ "Existing": "現有項目",
1627+ "expression_label_pattern": "[情緒_標籤].[圖檔_格式]",
1628+ "ext_translate_auto_mode": "自動翻譯模式",
1629+ "ext_translate_btn_chat": "翻譯聊天內容",
1630+ "ext_translate_btn_input": "翻譯輸入內容",
1631+ "ext_translate_clear": "清除翻譯",
1632+ "ext_translate_mode_both": "翻譯輸入和回應",
1633+ "ext_translate_mode_inputs": "僅翻譯輸入",
1634+ "ext_translate_mode_none": "無翻譯",
1635+ "ext_translate_mode_provider": "翻譯提供者",
1636+ "ext_translate_mode_responses": "僅翻譯回應",
1637+ "ext_translate_target_lang": "目標語言",
1638+ "ext_translate_title": "聊天翻譯",
1639+ "Extensions Menu": "擴充功能選單",
1640+ "Extras": "擴充功能",
1641+ "Extras API": "擴充功能 API",
1642+ "Featherless Model Selection": "無羽模型選擇",
1643+ "File vectorization settings": "檔案向量化設定",
1644+ "Filter to Characters or Tags": "角色或標籤篩選",
1645+ "First User Prefix": "第一使用者前綴",
1646+ "folder of your user data directory and name it as the name of the character.": "使用者資料目錄中的資料夾名稱應與角色名稱一致。",
1647+ "Group Scoring": "群組評分",
1648+ "Groups and Past Personas": "群組與過去的使用者角色設定",
1649+ "Hint:": "提示:",
1650+ "Hint: Set the URL in the API connection settings.": "提示:在 API 連線設置中設定 URL。",
1651+ "Horde": "Horde",
1652+ "HuggingFace Token": "HuggingFace 符元",
1653+ "Image Captioning": "圖片標註",
1654+ "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)",
1655+ "Injection Position": "插入位置",
1656+ "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",
1657+ "Injection Template": "插入範本",
1658+ "Insert#": "插入#",
1659+ "Instruct Sequences": "指令序列",
1660+ "Instruct Template": "指令範本",
1661+ "Interactive Mode": "互動模式",
1662+ "Karras": "Karras",
1663+ "Keep model in memory": "將模型保存在記憶體中",
1664+ "Keyboard": "鍵盤:",
1665+ "KoboldAI Horde Website": "KoboldAI Horde 網站",
1666+ "Last User Prefix": "最後用戶前綴",
1667+ "Linear": "線性",
1668+ "LLM": "LLM",
1669+ "LLM Prompt": "LLM 提示詞",
1670+ "Load a custom asset list or select": "載入或選擇自定義資源列表",
1671+ "Load an asset list": "載入資源列表",
1672+ "Local": "本地",
1673+ "Local (Transformers)": "本地(Transformers)",
1674+ "macro)": "巨集)",
1675+ "Main API": "主要 API",
1676+ "Markdown Hotkeys": "Markdown 快捷鍵",
1677+ "Master Export": "主匯出",
1678+ "Master Import": "主匯入",
1679+ "Max Entries": "最大條目數",
1680+ "Max Recursion Steps": "最大遞迴步數",
1681+ "Message attachments": "訊息附件",
1682+ "Message Template": "訊息模板",
1683+ "Model ID": "模型 ID",
1684+ "mui_reset": "重置",
1685+ "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)",
1686+ "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設置以切換模型。",
1687+ "Names as Stop Strings": "將名稱用作停止字串",
1688+ "Never": "從不",
1689+ "NomicAI API Key": "NomicAI API 金鑰",
1690+ "Non-recursable (will not be activated by another)": "不可遞迴(不會被其他條目啟動)",
1691+ "None (disabled)": "無(已禁用)",
1692+ "OK": "確定",
1693+ "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "舊訊息會在聊天時逐步向量化。\n若要處理所有先前訊息,請點擊下方按鈕。",
1694+ "Only used when Main API or WebLLM Extension is selected.": "僅在選擇主要 API 或 WebLLM 擴展時使用。",
1695+ "Open a chat to see the character expressions.": "開啟聊天以查看角色表情。",
1696+ "Post-History Instructions": "聊天歷史後指示",
1697+ "Prefer Character Card Instructions": "偏好角色卡聊天歷史後指示",
1698+ "Prioritize": "優先處理",
1699+ "Prompt Content": "提示詞內容",
1700+ "prompt_manager_in_chat": "聊天中的提示詞管理",
1701+ "prompt_post_processing_none": "無",
1702+ "Purge Vectors": "清除向量",
1703+ "Put images with expressions there. File names should follow the pattern:": "將表情圖片放置於此,檔案名稱應遵循以下格式:",
1704+ "Quad": "四元數",
1705+ "Query messages": "查詢訊息",
1706+ "Quick Impersonate button": "快速模擬按鈕",
1707+ "Recursion Level": "遞迴層級",
1708+ "Remove all image overrides": "移除所有圖片覆蓋",
1709+ "Restore default": "",
1710+ "Retain#": "保留#",
1711+ "Retrieve chunks": "檢索 Chunks",
1712+ "Sampler Order": "取樣順序",
1713+ "Score threshold": "分數閾值",
1714+ "sd_free_extend_small": "(互動/指令)",
1715+ "sd_free_extend_txt": "擴展自由模式提示詞",
1716+ "sd_function_tool_txt": "使用功能工具",
1717+ "sd_prompt_-1": "聊天訊息範本",
1718+ "sd_prompt_-2": "功能工具提示描述",
1719+ "sd_prompt_0": "角色(你,第二人稱)",
1720+ "sd_prompt_1": "使用者(我,第一人稱)",
1721+ "sd_prompt_10": "肖像(多模態模式)",
1722+ "sd_prompt_11": "自由模式(LLM 擴展)",
1723+ "sd_prompt_2": "場景(完整故事)",
1724+ "sd_prompt_3": "原始最後訊息",
1725+ "sd_prompt_4": "最後訊息",
1726+ "sd_prompt_5": "肖像(你,第二人稱)",
1727+ "sd_prompt_7": "背景",
1728+ "sd_prompt_8": "角色(多模態模式)",
1729+ "sd_prompt_9": "使用者(多模態模式)",
1730+ "Select a Model": "選擇模型",
1731+ "Select the API for classifying expressions.": "選擇分類表情的 API。",
1732+ "Select with Enter": "按 Enter 選擇",
1733+ "Select with Tab": "按 Tab 選擇",
1734+ "Select with Tab or Enter": "按 Tab 或 Enter 選擇",
1735+ "Separators as Stop Strings": "以分隔符作為停止字串",
1736+ "Set the default and fallback expression being used when no matching expression is found.": "設定在無法配對到表情時所使用的預設和替代圖片。",
1737+ "Set your API keys and endpoints in the API Connections tab first.": "請先在「API 連接」頁面中設定您的 API 密鑰和端點。",
1738+ "Show default images (emojis) if sprite missing": "無對應圖片時,顯示為預設表情符號(emoji)",
1739+ "Show group chat queue": "顯示群組聊天隊列",
1740+ "Size threshold (KB)": "大小閾值(KB)",
1741+ "Slash Command": "斜線命令",
1742+ "space_ slash command.": "斜線命令。",
1743+ "Sprite Folder Override": "表情立繪資料夾覆蓋",
1744+ "Sprite set:": "表情集:",
1745+ "Show Gallery": "查看畫廊",
1746+ "Sticky": "固定",
1747+ "Style Preset": "預設樣式",
1748+ "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理",
1749+ "Summarize chat messages when sending": "傳送時摘要聊天內容",
1750+ "Swipe # for All Messages": "為所有訊息分配滑動編號 #",
1751+ "System Message Sequences": "系統訊息順序",
1752+ "System Prefix": "系統訊息前綴",
1753+ "System Prompt Sequences": "系統提示詞順序",
1754+ "System Suffix": "系統訊息後綴",
1755+ "Tabby Model": "Tabby 模型",
1756+ "tag_import_all": "全部匯入",
1757+ "tag_import_none": "不匯入",
1758+ "Text Generation WebUI (oobabooga)": "文字生成 WebUI (oobabooga)",
1759+ "The server MUST be started with the --embedding flag to use this feature!": "若要使用此功能,伺服器必須啟動時加上 --embedding 標誌。",
1760+ "Threshold": "閾值",
1761+ "to install 3rd party extensions.": "用於安裝第三方擴充功能。",
1762+ "Top": "頂部",
1763+ "Translate text to English before classification": "在分類前將文本翻譯為英文。",
1764+ "Uncheck to hide the extensions messages in chat prompts.": "不勾選即可隱藏聊天提示詞中的擴充功能訊息。",
1765+ "Unchecked: only entries with ❌ status can be activated.": "未勾選時:僅允許啟用狀態為 ❌ 的條目。",
1766+ "Unified Sampling": "統一取樣(Unified Sampling)",
1767+ "Upload sprite pack (ZIP)": "打包上傳立繪(ZIP 格式)",
1768+ "Use a forward slash to specify a subfolder. Example: _space": "使用「/」來設置子目錄,例如:_space",
1769+ "Use ADetailer (Face)": "使用 ADetailer 進行臉部處理。",
1770+ "Use an admin API key.": "使用管理員的 API 密鑰。",
1771+ "Use global": "啟用全域設定",
1772+ "User Message Sequences": "使用者訊息順序",
1773+ "User Node Color": "使用者節點顏色",
1774+ "User Prefix": "使用者訊息前綴",
1775+ "User Suffix": "使用者訊息後綴",
1776+ "Using a proxy that youre not running yourself is a risk to your data privacy.": "使用非自行管理的代理服務存在數據隱私洩漏風險。",
1777+ "Vector Storage": "向量存儲",
1778+ "Vector Summarization": "向量摘要",
1779+ "Vectorization Model": "向量生成模型",
1780+ "Vectorization Source": "向量化來源",
1781+ "Vectorize All": "向量化全部數據",
1782+ "View Stats": "查看統計資料",
1783+ "Warning: This might cause your sent messages to take a bit to process and slow down response time.": "警告:這可能會導致訊息處理速度變慢,並延長回應時間。",
1784+ "WarningThis might cause your sent messages to take a bit to process and slow down response time.": "警告:這將顯著減緩向量生成速度,因為所有消息都需先進行摘要。",
1785+ "WebLLM Extension": "WebLLM 擴充功能",
1786+ "Whole Words": "匹配完整單字",
1787+ "Will be used if the API doesnt support JSON schemas or function calling.": "若 API 不支持 JSON 模式或函數調用,將使用此設定。",
1788+ "World Info settings": "世界資訊設定",
1789+ "You are in offline mode. Click on the image below to set the expression.": "您目前為離線狀態,請點擊下方圖片進行表情設定。",
1790+ "You can find your API key in the Stability AI dashboard.": "API 金鑰可在 Stability AI 儀表板中查看。"
14461791}
public/script.js+33 -133
@@ -13,7 +13,7 @@ import {
1313 default as libs,
1414} from './lib.js';
1515
1616import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods, shouldSendOnEnter } from './scripts/RossAscends-mods.js';
1717import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js';
1818import {
1919 generateKoboldWithStreaming,
@@ -37,8 +37,6 @@ import {
3737 parseTabbyLogprobs,
3838} from './scripts/textgen-settings.js';
3939
40-const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER, FEATHERLESS } = textgen_types;
41-
4240import {
4341 world_info,
4442 getWorldInfoPrompt,
@@ -67,9 +65,7 @@ import {
6765 getGroupChat,
6866 renameGroupMember,
6967 createNewGroupChat,
70- getGroupPastChats,
7168 getGroupAvatar,
72- openGroupChat,
7369 editGroup,
7470 deleteGroupChat,
7571 renameGroupChat,
@@ -98,7 +94,6 @@ import {
9894 resetMovableStyles,
9995 forceCharacterEditorTokenize,
10096 applyPowerUserSettings,
101- switchSwipeNumAllMessages,
10297} from './scripts/power-user.js';
10398
10499import {
@@ -174,8 +169,8 @@ import {
174169} from './scripts/utils.js';
175170import { debounce_timeout } from './scripts/constants.js';
176171
177172import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
178173import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
179174import {
180175 tag_map,
181176 tags,
@@ -225,8 +220,8 @@ import {
225220 instruct_presets,
226221 selectContextPreset,
227222} from './scripts/instruct-mode.js';
228223import { initLocales, t, translate } from './scripts/i18n.js';
229224import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';
230225import {
231226 user_avatar,
232227 getUserAvatars,
@@ -242,11 +237,11 @@ import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay
242237import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels } from './scripts/textgen-models.js';
243238import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js';
244239import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
245240import { MacrosParser, evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
246241import { currentUser, setUserControls } from './scripts/user.js';
247242import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
248243import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
249244import { initScrapers, ScraperManager } from './scripts/scrapers.js';
250245import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
251246import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
252247import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -268,6 +263,13 @@ import { initServerHistory } from './scripts/server-history.js';
268263import { initSettingsSearch } from './scripts/setting-search.js';
269264import { initBulkEdit } from './scripts/bulk-edit.js';
270265import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
266+import { getContext } from './scripts/st-context.js';
267+
268+// API OBJECT FOR EXTERNAL WIRING
269+globalThis.SillyTavern = {
270+ libs,
271+ getContext,
272+};
271273
272274//exporting functions and vars for mods
273275export {
@@ -427,10 +429,6 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
427429 }
428430});
429431
430-// API OBJECT FOR EXTERNAL WIRING
431-window['SillyTavern'] = {};
432-window['SillyTavern'].libs = libs;
433-
434432// Event source init
435433export const event_types = {
436434 APP_READY: 'app_ready',
@@ -536,7 +534,7 @@ let displayVersion = 'SillyTavern';
536534
537535let generatedPromptCache = '';
538536let generation_started = new Date();
539537/** @type {import('./scripts/char-data.js').v1CharData[]} */
540538export let characters = [];
541539export let this_chid;
542540let saveCharactersPage = 0;
@@ -812,7 +810,7 @@ export let menu_type = '';
812810export let selected_button = ''; //which button pressed
813811
814812//create pole save
815813export let create_save = {
816814 name: '',
817815 description: '',
818816 creator_notes: '',
@@ -864,7 +862,7 @@ export let amount_gen = 80; //default max length of AI generated responses
864862export let max_context = 2048;
865863
866864var swipes = true;
867865export let extension_prompts = {};
868866
869867export let main_api;// = "kobold";
870868//novel settings
@@ -1175,7 +1173,7 @@ async function getStatusTextgen() {
11751173 return resultCheckStatus();
11761174 }
11771175
11781176 if (textgen_settings.type == textgen_types.OOBA && textgen_settings.bypass_status_check) {
11791177 setOnlineStatus('Status check bypassed');
11801178 return resultCheckStatus();
11811179 }
@@ -1193,34 +1191,34 @@ async function getStatusTextgen() {
11931191
11941192 const data = await response.json();
11951193
11961194 if (textgen_settings.type === textgen_types.MANCER) {
11971195 loadMancerModels(data?.data);
11981196 setOnlineStatus(textgen_settings.mancer_model);
11991197 } else if (textgen_settings.type === textgen_types.TOGETHERAI) {
12001198 loadTogetherAIModels(data?.data);
12011199 setOnlineStatus(textgen_settings.togetherai_model);
12021200 } else if (textgen_settings.type === textgen_types.OLLAMA) {
12031201 loadOllamaModels(data?.data);
12041202 setOnlineStatus(textgen_settings.ollama_model || 'Connected');
12051203 } else if (textgen_settings.type === textgen_types.INFERMATICAI) {
12061204 loadInfermaticAIModels(data?.data);
12071205 setOnlineStatus(textgen_settings.infermaticai_model);
12081206 } else if (textgen_settings.type === textgen_types.DREAMGEN) {
12091207 loadDreamGenModels(data?.data);
12101208 setOnlineStatus(textgen_settings.dreamgen_model);
12111209 } else if (textgen_settings.type === textgen_types.OPENROUTER) {
12121210 loadOpenRouterModels(data?.data);
12131211 setOnlineStatus(textgen_settings.openrouter_model);
12141212 } else if (textgen_settings.type === textgen_types.VLLM) {
12151213 loadVllmModels(data?.data);
12161214 setOnlineStatus(textgen_settings.vllm_model);
12171215 } else if (textgen_settings.type === textgen_types.APHRODITE) {
12181216 loadAphroditeModels(data?.data);
12191217 setOnlineStatus(textgen_settings.aphrodite_model);
12201218 } else if (textgen_settings.type === textgen_types.FEATHERLESS) {
12211219 loadFeatherlessModels(data?.data);
12221220 setOnlineStatus(textgen_settings.featherless_model);
12231221 } else if (textgen_settings.type === textgen_types.TABBY) {
12241222 loadTabbyModels(data?.data);
12251223 setOnlineStatus(textgen_settings.tabby_model || data?.result);
12261224 } else {
@@ -5582,7 +5580,7 @@ function extractMultiSwipes(data, type) {
55825580 return swipes;
55835581 }
55845582
55855583 if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [textgen_types.MANCER, textgen_types.VLLM, textgen_types.APHRODITE, textgen_types.TABBY, textgen_types.INFERMATICAI].includes(textgen_settings.type))) {
55865584 if (!Array.isArray(data.choices)) {
55875585 return swipes;
55885586 }
@@ -5745,7 +5743,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
57455743 return getMessage;
57465744}
57475745
57485746export async function saveReply(type, getMessage, fromStreaming, title, swipes) {
57495747 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||
57505748 chat[chat.length - 1]['is_user'])) {
57515749 type = 'normal';
@@ -7706,14 +7704,12 @@ export function showSwipeButtons() {
77067704 //allows for writing individual swipe counters for past messages
77077705 const lastSwipeCounter = $('.last_mes .swipes-counter');
77087706 lastSwipeCounter.text(swipeCounterText).show();
7709-
7710- switchSwipeNumAllMessages();
77117707}
77127708
77137709export function hideSwipeButtons() {
77147710 $('#chat')chatElement.find('.swipe_right').hide();
77157711 $('#chat')chatElement.find('.last_mes .swipes-counter').hide();
77167712 $('#chat')chatElement.find('.swipe_left').hide();
77177713}
77187714
77197715/**
@@ -8279,102 +8275,6 @@ async function createOrEditCharacter(e) {
82798275 }
82808276}
82818277
8282-window['SillyTavern'].getContext = function () {
8283- return {
8284- chat: chat,
8285- characters: characters,
8286- groups: groups,
8287- name1: name1,
8288- name2: name2,
8289- characterId: this_chid,
8290- groupId: selected_group,
8291- chatId: selected_group
8292- ? groups.find(x => x.id == selected_group)?.chat_id
8293- : (this_chid && characters[this_chid] && characters[this_chid].chat),
8294- getCurrentChatId: getCurrentChatId,
8295- getRequestHeaders: getRequestHeaders,
8296- reloadCurrentChat: reloadCurrentChat,
8297- renameChat: renameChat,
8298- saveSettingsDebounced: saveSettingsDebounced,
8299- onlineStatus: online_status,
8300- maxContext: Number(max_context),
8301- chatMetadata: chat_metadata,
8302- streamingProcessor,
8303- eventSource: eventSource,
8304- eventTypes: event_types,
8305- addOneMessage: addOneMessage,
8306- generate: Generate,
8307- sendStreamingRequest: sendStreamingRequest,
8308- sendGenerationRequest: sendGenerationRequest,
8309- stopGeneration: stopGeneration,
8310- getTokenCount: getTokenCount,
8311- extensionPrompts: extension_prompts,
8312- setExtensionPrompt: setExtensionPrompt,
8313- updateChatMetadata: updateChatMetadata,
8314- saveChat: saveChatConditional,
8315- openCharacterChat: openCharacterChat,
8316- openGroupChat: openGroupChat,
8317- saveMetadata: saveMetadata,
8318- sendSystemMessage: sendSystemMessage,
8319- activateSendButtons,
8320- deactivateSendButtons,
8321- saveReply,
8322- substituteParams,
8323- substituteParamsExtended,
8324- SlashCommandParser,
8325- SlashCommand,
8326- SlashCommandArgument,
8327- SlashCommandNamedArgument,
8328- ARGUMENT_TYPE,
8329- executeSlashCommandsWithOptions,
8330- /** @deprecated Use SlashCommandParser.addCommandObject() instead */
8331- registerSlashCommand: registerSlashCommand,
8332- /** @deprecated Use executeSlashCommandWithOptions instead */
8333- executeSlashCommands: executeSlashCommands,
8334- timestampToMoment: timestampToMoment,
8335- /** @deprecated Handlebars for extensions are no longer supported. */
8336- registerHelper: () => { },
8337- registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
8338- unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
8339- registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
8340- unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
8341- isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
8342- canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
8343- registerDebugFunction: registerDebugFunction,
8344- /** @deprecated Use renderExtensionTemplateAsync instead. */
8345- renderExtensionTemplate: renderExtensionTemplate,
8346- renderExtensionTemplateAsync: renderExtensionTemplateAsync,
8347- registerDataBankScraper: ScraperManager.registerDataBankScraper,
8348- /** @deprecated Use callGenericPopup or Popup instead. */
8349- callPopup: callPopup,
8350- callGenericPopup: callGenericPopup,
8351- showLoader: showLoader,
8352- hideLoader: hideLoader,
8353- mainApi: main_api,
8354- extensionSettings: extension_settings,
8355- ModuleWorkerWrapper: ModuleWorkerWrapper,
8356- getTokenizerModel: getTokenizerModel,
8357- generateQuietPrompt: generateQuietPrompt,
8358- writeExtensionField: writeExtensionField,
8359- getThumbnailUrl: getThumbnailUrl,
8360- selectCharacterById: selectCharacterById,
8361- messageFormatting: messageFormatting,
8362- shouldSendOnEnter: shouldSendOnEnter,
8363- isMobile: isMobile,
8364- t: t,
8365- translate: translate,
8366- tags: tags,
8367- tagMap: tag_map,
8368- menuType: menu_type,
8369- createCharacterData: create_save,
8370- /** @deprecated Legacy snake-case naming, compatibility with old extensions */
8371- event_types: event_types,
8372- Popup: Popup,
8373- POPUP_TYPE: POPUP_TYPE,
8374- POPUP_RESULT: POPUP_RESULT,
8375- };
8376-};
8377-
83788278/**
83798279 * Formats a counter for a swipe view.
83808280 * @param {number} current The current number of items.
public/scripts/char-data.js+1 -0
@@ -75,6 +75,7 @@
7575 * @property {string} [source_url] - The source URL associated with the character.
7676 * @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
7777 * @property {{source: string[]}} [risuai] - The RisuAI-specific data associated with the character.
78+ * @property {{positive: string, negative: string}} [sd_character_prompt] - SD-specific data associated with the character.
7879 */
7980
8081/**
public/scripts/extensions.js+491 -217
@@ -4,28 +4,57 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
44import { showLoader } from './loader.js';
55import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
66import { renderTemplate, renderTemplateAsync } from './templates.js';
77import { delay, isSubsetOf, setValueByPath } from './utils.js';
8+import { getContext } from './st-context.js';
9+import { isAdmin } from './user.js';
10+import { t } from './i18n.js';
11+import { debounce_timeout } from './constants.js';
12+
813export {
914 getContext,
1015 getApiUrl,
11- loadExtensionSettings,
12- runGenerationInterceptors,
13- doExtrasFetch,
14- modules,
15- extension_settings,
16- ModuleWorkerWrapper,
1716};
1817
1918/** @type {string[]} */
2019export let extensionNames = [];
2120
21+/**
22+ * Holds the type of each extension.
23+ * Don't use this directly, use getExtensionType instead!
24+ * @type {Record<string, string>}
25+ */
26+export let extensionTypes = {};
27+
28+/**
29+ * A list of active modules provided by the Extras API.
30+ * @type {string[]}
31+ */
32+export let modules = [];
33+
34+/**
35+ * A set of active extensions.
36+ * @type {Set<string>}
37+ */
38+let activeExtensions = new Set();
39+
40+const getApiUrl = () => extension_settings.apiUrl;
41+const sortManifests = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));
42+let connectedToApi = false;
43+
44+/**
45+ * Holds manifest data for each extension.
46+ * @type {Record<string, object>}
47+ */
2248let manifests = {};
23-const defaultUrl = 'http://localhost:5100';
2449
25-let saveMetadataTimeout = null;
50+/**
51+ * Default URL for the Extras API.
52+ */
53+const defaultUrl = 'http://localhost:5100';
2654
2755let requiresReload = false;
2856let stateChanged = false;
57+let saveMetadataTimeout = null;
2958
3059export function saveMetadataDebounced() {
3160 const context = getContext();
@@ -50,9 +79,9 @@ export function saveMetadataDebounced() {
5079 }
5180
5281 console.debug('Saving metadata...');
5382 await newContext.saveMetadata();
5483 console.debug('Saved metadata...');
5584 }, 1000debounce_timeout.relaxed);
5685}
5786
5887/**
@@ -82,7 +111,7 @@ export function renderExtensionTemplateAsync(extensionName, templateId, template
82111}
83112
84113// Disables parallel updates
85114export class ModuleWorkerWrapper {
86115 constructor(callback) {
87116 this.isBusy = false;
88117 this.callback = callback;
@@ -106,7 +135,7 @@ class ModuleWorkerWrapper {
106135 }
107136}
108137
109138export const extension_settings = {
110139 apiUrl: defaultUrl,
111140 apiKey: '',
112141 autoConnect: false,
@@ -171,13 +200,6 @@ const extension_settings = {
171200 disabled_attachments: [],
172201};
173202
174-let modules = [];
175-let activeExtensions = new Set();
176-
177-const getContext = () => window['SillyTavern'].getContext();
178-const getApiUrl = () => extension_settings.apiUrl;
179-let connectedToApi = false;
180-
181203function showHideExtensionsMenu() {
182204 // Get the number of menu items that are not hidden
183205 const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0;
@@ -194,7 +216,23 @@ function showHideExtensionsMenu() {
194216// Periodically check for new extensions
195217const menuInterval = setInterval(showHideExtensionsMenu, 1000);
196218
197-async function doExtrasFetch(endpoint, args) {
219+/**
220+ * Gets the type of an extension based on its external ID.
221+ * @param {string} externalId External ID of the extension (excluding or including the leading 'third-party/')
222+ * @returns {string} Type of the extension (global, local, system, or empty string if not found)
223+ */
224+function getExtensionType(externalId) {
225+ const id = Object.keys(extensionTypes).find(id => id === externalId || (id.startsWith('third-party') && id.endsWith(externalId)));
226+ return id ? extensionTypes[id] : '';
227+}
228+
229+/**
230+ * Performs a fetch of the Extras API.
231+ * @param {string|URL} endpoint Extras API endpoint
232+ * @param {RequestInit} args Request arguments
233+ * @returns {Promise<Response>} Response from the fetch
234+ */
235+export async function doExtrasFetch(endpoint, args = {}) {
198236 if (!args) {
199237 args = {};
200238 }
@@ -213,10 +251,13 @@ async function doExtrasFetch(endpoint, args) {
213251 });
214252 }
215253
216254 const response =return await fetch(endpoint, args);
217- return response;
218255}
219256
257+/**
258+ * Discovers extensions from the API.
259+ * @returns {Promise<{name: string, type: string}[]>}
260+ */
220261async function discoverExtensions() {
221262 try {
222263 const response = await fetch('/api/extensions/discover');
@@ -245,6 +286,11 @@ function onEnableExtensionClick() {
245286 enableExtension(name, false);
246287}
247288
289+/**
290+ * Enables an extension by name.
291+ * @param {string} name Extension name
292+ * @param {boolean} [reload=true] If true, reload the page after enabling the extension
293+ */
248294export async function enableExtension(name, reload = true) {
249295 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
250296 stateChanged = true;
@@ -256,6 +302,11 @@ export async function enableExtension(name, reload = true) {
256302 }
257303}
258304
305+/**
306+ * Disables an extension by name.
307+ * @param {string} name Extension name
308+ * @param {boolean} [reload=true] If true, reload the page after disabling the extension
309+ */
259310export async function disableExtension(name, reload = true) {
260311 extension_settings.disabledExtensions.push(name);
261312 stateChanged = true;
@@ -267,6 +318,11 @@ export async function disableExtension(name, reload = true) {
267318 }
268319}
269320
321+/**
322+ * Loads manifest.json files for extensions.
323+ * @param {string[]} names Array of extension names
324+ * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values
325+ */
270326async function getManifests(names) {
271327 const obj = {};
272328 const promises = [];
@@ -294,43 +350,36 @@ async function getManifests(names) {
294350 return obj;
295351}
296352
353+/**
354+ * Tries to activate all available extensions that are not already active.
355+ * @returns {Promise<void>}
356+ */
297357async function activateExtensions() {
298358 const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1].loading_order -, b[1].loading_order));
299359 const promises = [];
300360
301361 for (let entry of extensions) {
302362 const name = entry[0];
303363 const manifest = entry[1];
304- const elementExists = document.getElementById(name) !== null;
305364
306365 if (elementExists || activeExtensions.has(name)) {
307366 continue;
308367 }
309368
310- // all required modules are active (offline extensions require none)
369+ const meetsModuleRequirements = !Array.isArray(manifest.requires) || isSubsetOf(modules, manifest.requires);
311- if (isSubsetOf(modules, manifest.requires)) {
370+ const isDisabled = extension_settings.disabledExtensions.includes(name);
312- try {
313- const isDisabled = extension_settings.disabledExtensions.includes(name);
314- const li = document.createElement('li');
315-
316- if (!isDisabled) {
317- const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);
318- await promise
319- .then(() => activeExtensions.add(name))
320- .catch(err => console.log('Could not activate extension: ' + name, err));
321- promises.push(promise);
322- }
323- else {
324- li.classList.add('disabled');
325- }
326-
327- li.id = name;
328- li.innerText = manifest.display_name;
329371
330- $('#extensions_list').append(li);
372+ if (meetsModuleRequirements && !isDisabled) {
373+ try {
374+ console.debug('Activating extension', name);
375+ const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);
376+ await promise
377+ .then(() => activeExtensions.add(name))
378+ .catch(err => console.log('Could not activate extension', name, err));
379+ promises.push(promise);
331380 }
332381 catch (error) {
333382 console.error(`'Could not activate extension:', ${name}`);
334383 console.error(error);
335384 }
336385 }
@@ -340,8 +389,8 @@ async function activateExtensions() {
340389}
341390
342391async function connectClickHandler() {
343392 const baseUrl = String($('#extensions_url').val());
344393 extension_settings.apiUrl = String(baseUrl);
345394 const testApiKey = $('#extensions_api_key').val();
346395 extension_settings.apiKey = String(testApiKey);
347396 saveSettingsDebounced();
@@ -401,21 +450,11 @@ function notifyUpdatesInputHandler() {
401450 }
402451}
403452
404-/* $(document).on('click', function (e) {
453+/**
405- const target = $(e.target);
454+ * Connects to the Extras API.
406- if (target.is(dropdown)) return;
455+ * @param {string} baseUrl Extras API base URL
407- if (target.is(button) && dropdown.is(':hidden')) {
456+ * @returns {Promise<void>}
408- dropdown.toggle(200);
457+ */
409- popper.update();
410- }
411- if (target !== dropdown &&
412- target !== button &&
413- dropdown.is(":visible")) {
414- dropdown.hide(200);
415- }
416- });
417-} */
418-
419458async function connectToApi(baseUrl) {
420459 if (!baseUrl) {
421460 return;
@@ -431,7 +470,7 @@ async function connectToApi(baseUrl) {
431470 const data = await getExtensionsResult.json();
432471 modules = data.modules;
433472 await activateExtensions();
434473 await eventSource.emit(event_types.EXTRAS_CONNECTED, modules);
435474 }
436475
437476 updateStatus(getExtensionsResult.ok);
@@ -441,71 +480,84 @@ async function connectToApi(baseUrl) {
441480 }
442481}
443482
483+/**
484+ * Updates the status of Extras API connection.
485+ * @param {boolean} success Whether the connection was successful
486+ */
444487function updateStatus(success) {
445488 connectedToApi = success;
446489 const _text = success ? 't`Connected to API'` : 't`Could not connect to API'`;
447490 const _class = success ? 'success' : 'failure';
448491 $('#extensions_status').text(_text);
449492 $('#extensions_status').attr('class', _class);
450493}
451494
495+/**
496+ * Adds a CSS file for an extension.
497+ * @param {string} name Extension name
498+ * @param {object} manifest Extension manifest
499+ * @returns {Promise<void>} When the CSS is loaded
500+ */
452501function addExtensionStyle(name, manifest) {
453502 if (!manifest.css) {
454- return new Promise((resolve, reject) => {
503+ return Promise.resolve();
455- const url = `/scripts/extensions/${name}/${manifest.css}`;
456-
457- if ($(`link[id="${name}"]`).length === 0) {
458- const link = document.createElement('link');
459- link.id = name;
460- link.rel = 'stylesheet';
461- link.type = 'text/css';
462- link.href = url;
463- link.onload = function () {
464- resolve();
465- };
466- link.onerror = function (e) {
467- reject(e);
468- };
469- document.head.appendChild(link);
470- }
471- });
472504 }
473505
474- return Promise.resolve();
506+ return new Promise((resolve, reject) => {
507+ const url = `/scripts/extensions/${name}/${manifest.css}`;
508+
509+ if ($(`link[id="${name}"]`).length === 0) {
510+ const link = document.createElement('link');
511+ link.id = name;
512+ link.rel = 'stylesheet';
513+ link.type = 'text/css';
514+ link.href = url;
515+ link.onload = function () {
516+ resolve();
517+ };
518+ link.onerror = function (e) {
519+ reject(e);
520+ };
521+ document.head.appendChild(link);
522+ }
523+ });
475524}
476525
526+/**
527+ * Loads a JS file for an extension.
528+ * @param {string} name Extension name
529+ * @param {object} manifest Extension manifest
530+ * @returns {Promise<void>} When the script is loaded
531+ */
477532function addExtensionScript(name, manifest) {
478533 if (!manifest.js) {
479- return new Promise((resolve, reject) => {
534+ return Promise.resolve();
480- const url = `/scripts/extensions/${name}/${manifest.js}`;
481- let ready = false;
482-
483- if ($(`script[id="${name}"]`).length === 0) {
484- const script = document.createElement('script');
485- script.id = name;
486- script.type = 'module';
487- script.src = url;
488- script.async = true;
489- script.onerror = function (err) {
490- reject(err, script);
491- };
492- script.onload = script.onreadystatechange = function () {
493- // console.log(this.readyState); // uncomment this line to see which ready states are called.
494- if (!ready && (!this.readyState || this.readyState == 'complete')) {
495- ready = true;
496- resolve();
497- }
498- };
499- document.body.appendChild(script);
500- }
501- });
502535 }
503536
504- return Promise.resolve();
537+ return new Promise((resolve, reject) => {
538+ const url = `/scripts/extensions/${name}/${manifest.js}`;
539+ let ready = false;
540+
541+ if ($(`script[id="${name}"]`).length === 0) {
542+ const script = document.createElement('script');
543+ script.id = name;
544+ script.type = 'module';
545+ script.src = url;
546+ script.async = true;
547+ script.onerror = function (err) {
548+ reject(err);
549+ };
550+ script.onload = function () {
551+ if (!ready) {
552+ ready = true;
553+ resolve();
554+ }
555+ };
556+ document.body.appendChild(script);
557+ }
558+ });
505559}
506560
507-
508-
509561/**
510562 * Generates HTML string for displaying an extension in the UI.
511563 *
@@ -515,64 +567,85 @@ function addExtensionScript(name, manifest) {
515567 * @param {boolean} isDisabled - Whether the extension is disabled or not.
516568 * @param {boolean} isExternal - Whether the extension is external or not.
517569 * @param {string} checkboxClass - The class for the checkbox HTML element.
518570 * @return {Promise<string>} - The HTML string that represents the extension.
519571 */
520572async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
573+ function getExtensionIcon() {
574+ const type = getExtensionType(name);
575+ switch (type) {
576+ case 'global':
577+ return '<i class="fa-sm fa-fw fa-solid fa-server" data-i18n="[title]ext_type_global" title="This is a global extension, available for all users."></i>';
578+ case 'local':
579+ return '<i class="fa-sm fa-fw fa-solid fa-user" data-i18n="[title]ext_type_local" title="This is a local extension, available only for you."></i>';
580+ case 'system':
581+ return '<i class="fa-sm fa-fw fa-solid fa-cog" data-i18n="[title]ext_type_system" title="This is a built-in extension. It cannot be deleted and updates with the app."></i>';
582+ default:
583+ return '<i class="fa-sm fa-fw fa-solid fa-question" title="Unknown extension type."></i>';
584+ }
585+ }
586+
587+ const isUserAdmin = isAdmin();
588+ const extensionIcon = getExtensionIcon();
521589 const displayName = manifest.display_name;
522590 letconst displayVersion = manifest.version ? ` v${manifest.version}` :|| '';
523- let isUpToDate = true;
591+ const externalId = name.replace('third-party', '');
524- let updateButton = '';
525592 let originHtml = '';
526593 if (isExternal) {
527- let data = await getExtensionVersion(name.replace('third-party', ''));
594+ originHtml = '<a>';
528- let branch = data.currentBranchName;
529- let commitHash = data.currentCommitHash;
530- let origin = data.remoteUrl;
531- isUpToDate = data.isUpToDate;
532- displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`;
533- updateButton = isUpToDate ?
534- `<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Up to date"><i class="fa-solid fa-code-commit fa-fw"></i></button></span>` :
535- `<span class="update-button"><button class="btn_update menu_button" data-name="${name.replace('third-party', '')}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button></span>`;
536- originHtml = `<a href="${origin}" target="_blank" rel="noopener noreferrer">`;
537595 }
538596
539597 let toggleElement = isActive || isDisabled ?
540598 `<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
541599 `<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
542600
543601 let deleteButton = isExternal ? `<span class="delete-button"><button class="btn_delete menu_button" data-name="${name.replace('third-party', '')externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button></span>` : '';
544-
602+ let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
545- // if external, wrap the name in a link to the repo
603+ let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
546-
604+ let modulesInfo = '';
547- let extensionHtml = `<hr>
548- <h4>
549- ${updateButton}
550- ${deleteButton}
551- ${originHtml}
552- <span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}">
553- ${DOMPurify.sanitize(displayName)}${displayVersion}
554- </span>
555- ${isExternal ? '</a>' : ''}
556-
557- <span style="float:right;">${toggleElement}</span>
558- </h4>`;
559605
560606 if (isActive && Array.isArray(manifest.optional)) {
561607 const optional = new Set(manifest.optional);
562608 modules.forEach(x => optional.delete(x));
563609 if (optional.size > 0) {
564610 const optionalString = DOMPurify.sanitize([...optional].join(', '));
565611 extensionHtmlmodulesInfo += `<pdiv class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></pdiv>`;
566612 }
567613 } else if (!isDisabled) { // Neither active nor disabled
568614 const requirements = new Set(manifest.requires);
569615 modules.forEach(x => requirements.delete(x));
570616 if (requirements.size > 0) {
571617 const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
572618 extensionHtmlmodulesInfo += `<pdiv class="extension_modules">Missing modules: <span class="failure">${requirementsString}</span></pdiv>`;
573619 }
574620 }
575621
622+ // if external, wrap the name in a link to the repo
623+
624+ let extensionHtml = `
625+ <div class="extension_block" data-name="${externalId}">
626+ <div class="extension_toggle">
627+ ${toggleElement}
628+ </div>
629+ <div class="extension_icon">
630+ ${extensionIcon}
631+ </div>
632+ <div class="flexGrow extension_text_block">
633+ ${originHtml}
634+ <span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}">
635+ <span class="extension_name">${DOMPurify.sanitize(displayName)}</span>
636+ <span class="extension_version">${DOMPurify.sanitize(displayVersion)}</span>
637+ ${modulesInfo}
638+ </span>
639+ ${isExternal ? '</a>' : ''}
640+ </div>
641+
642+ <div class="extension_actions flex-container alignItemsCenter">
643+ ${updateButton}
644+ ${moveButton}
645+ ${deleteButton}
646+ </div>
647+ </div>`;
648+
576649 return extensionHtml;
577650}
578651
@@ -580,9 +653,9 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
580653 * Gets extension data and generates the corresponding HTML for displaying the extension.
581654 *
582655 * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.
583656 * @return {Promise<object>} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string.
584657 */
585658async function getExtensionData(extension) {
586659 const name = extension[0];
587660 const manifest = extension[1];
588661 const isActive = activeExtensions.has(name);
@@ -591,7 +664,7 @@ async function getExtensionData(extension) {
591664
592665 const checkboxClass = isDisabled ? 'checkbox_disabled' : '';
593666
594667 const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass);
595668
596669 return { isExternal, extensionHtml };
597670}
@@ -614,42 +687,38 @@ function getModuleInformation() {
614687 * Generates the HTML strings for all extensions and displays them in a popup.
615688 */
616689async function showExtensionsDetails() {
690+ const abortController = new AbortController();
617691 let popupPromise;
618692 try {
619- const htmlDefault = $('<h3>Built-in Extensions:</h3>');
693+ // If we are updating an extension, the "old" popup is still active. We should close that.
620- const htmlExternal = $('<h3>Installed Extensions:</h3>').addClass('opacity50p');
694+ let initialScrollTop = 0;
621- const htmlLoading = $(`<h3 class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
695+ const oldPopup = Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'));
696+ if (oldPopup) {
697+ initialScrollTop = oldPopup.content.scrollTop;
698+ await oldPopup.completeCancelled();
699+ }
700+ const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>');
701+ const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3></div>');
702+ const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
622703 <i class="fa-solid fa-spinner fa-spin"></i>
623704 <span>Loading third-party extensions... Please wait...</span>
624705 </h3div>`);
625706
626- /** @type {Promise<any>[]} */
707+ htmlExternal.append(htmlLoading);
627- const promises = [];
628- const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
629708
630- for (const extension of extensions) {
709+ const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1])).map(getExtensionData);
631- promises.push(getExtensionData(extension));
632- }
633-
634- promises.forEach(promise => {
635- promise.then(value => {
636- const { isExternal, extensionHtml } = value;
637- const container = isExternal ? htmlExternal : htmlDefault;
638- container.append(extensionHtml);
639- });
640- });
641710
642711 Promise.allSettled(promises)extensions.then(forEach()value => {
643- htmlLoading.remove();
712+ const { isExternal, extensionHtml } = value;
644- htmlExternal.removeClass('opacity50p');
713+ const container = isExternal ? htmlExternal : htmlDefault;
714+ container.append(extensionHtml);
645715 });
646716
647717 const html = $('<div></div>')
648718 .addClass('extensions_info')
649- .append(getModuleInformation())
650719 .append(htmlDefault)
651720 .append(htmlLoadinghtmlExternal)
652721 .append(htmlExternalgetModuleInformation());
653722
654723 /** @type {import('./popup.js').CustomPopupButton} */
655724 const updateAllButton = {
@@ -662,12 +731,6 @@ async function showExtensionsDetails() {
662731 },
663732 };
664733
665- // If we are updating an extension, the "old" popup is still active. We should close that.
666- const oldPopup = Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'));
667- if (oldPopup) {
668- await oldPopup.complete(POPUP_RESULT.CANCELLED);
669- }
670-
671734 let waitingForSave = false;
672735
673736 const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
@@ -682,7 +745,7 @@ async function showExtensionsDetails() {
682745 }
683746 if (stateChanged) {
684747 waitingForSave = true;
685748 const toast = toastr.info('t`The page will be reloaded shortly...'`, 't`Extensions state changed'`);
686749 await saveSettings();
687750 toastr.clear(toast);
688751 waitingForSave = false;
@@ -692,12 +755,15 @@ async function showExtensionsDetails() {
692755 },
693756 });
694757 popupPromise = popup.show();
758+ popup.content.scrollTop = initialScrollTop;
759+ checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove());
695760 } catch (error) {
696761 toastr.error('t`Error loading extensions. See browser console for details.'`);
697762 console.error(error);
698763 }
699764 if (popupPromise) {
700765 await popupPromise;
766+ abortController.abort();
701767 }
702768 if (requiresReload) {
703769 showLoader();
@@ -705,17 +771,26 @@ async function showExtensionsDetails() {
705771 }
706772}
707773
708-
709774/**
710775 * Handles the click event for the update button of an extension.
711776 * This function makes a POST request to '/update_extensionapi/extensions/update' with the extension's name.
712777 * If the extension is already up to date, it displays a success message.
713778 * If the extension is not up to date, it updates the extension and displays a success message with the new commit hash.
714779 */
715780async function onUpdateClick() {
781+ const isCurrentUserAdmin = isAdmin();
716782 const extensionName = $(this).data('name');
717- $(this).find('i').addClass('fa-spin');
783+ const isGlobal = getExtensionType(extensionName) === 'global';
784+ if (isGlobal && !isCurrentUserAdmin) {
785+ toastr.error(t`You don't have permission to update global extensions.`);
786+ return;
787+ }
788+
789+ const icon = $(this).find('i');
790+ icon.addClass('fa-spin');
718791 await updateExtension(extensionName, false);
792+ // updateExtension eats the error, but we can at least stop the spinner
793+ icon.removeClass('fa-spin');
719794}
720795
721796/**
@@ -728,13 +803,23 @@ async function updateExtension(extensionName, quiet) {
728803 const response = await fetch('/api/extensions/update', {
729804 method: 'POST',
730805 headers: getRequestHeaders(),
731806 body: JSON.stringify({ extensionName }),
807+ extensionName,
808+ global: getExtensionType(extensionName) === 'global',
809+ }),
732810 });
733811
812+ if (!response.ok) {
813+ const text = await response.text();
814+ toastr.error(text || response.statusText, t`Extension update failed`, { timeOut: 5000 });
815+ console.error('Extension update failed', response.status, response.statusText, text);
816+ return;
817+ }
818+
734819 const data = await response.json();
735820
736821 if (!quiet) {
737822 void showExtensionsDetails();
738823 }
739824
740825 if (data.isUpToDate) {
@@ -757,44 +842,122 @@ async function updateExtension(extensionName, quiet) {
757842 */
758843async function onDeleteClick() {
759844 const extensionName = $(this).data('name');
845+ const isCurrentUserAdmin = isAdmin();
846+ const isGlobal = getExtensionType(extensionName) === 'global';
847+ if (isGlobal && !isCurrentUserAdmin) {
848+ toastr.error(t`You don't have permission to delete global extensions.`);
849+ return;
850+ }
851+
760852 // use callPopup to create a popup for the user to confirm before delete
761853 const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});
762854 if (confirmation === POPUP_RESULT.AFFIRMATIVE) {
763855 await deleteExtension(extensionName);
764856 }
765857}
766858
859+async function onMoveClick() {
860+ const extensionName = $(this).data('name');
861+ const isCurrentUserAdmin = isAdmin();
862+ const isGlobal = getExtensionType(extensionName) === 'global';
863+ if (isGlobal && !isCurrentUserAdmin) {
864+ toastr.error(t`You don't have permission to move extensions.`);
865+ return;
866+ }
867+
868+ const source = getExtensionType(extensionName);
869+ const destination = source === 'global' ? 'local' : 'global';
870+
871+ const confirmationHeader = t`Move extension`;
872+ const confirmationText = source == 'global'
873+ ? t`Are you sure you want to move ${extensionName} to your local extensions? This will make it available only for you.`
874+ : t`Are you sure you want to move ${extensionName} to the global extensions? This will make it available for all users.`;
875+
876+ const confirmation = await Popup.show.confirm(confirmationHeader, confirmationText);
877+
878+ if (!confirmation) {
879+ return;
880+ }
881+
882+ $(this).find('i').addClass('fa-spin');
883+ await moveExtension(extensionName, source, destination);
884+}
885+
886+/**
887+ * Moves an extension via the API.
888+ * @param {string} extensionName Extension name
889+ * @param {string} source Source type
890+ * @param {string} destination Destination type
891+ * @returns {Promise<void>}
892+ */
893+async function moveExtension(extensionName, source, destination) {
894+ try {
895+ const result = await fetch('/api/extensions/move', {
896+ method: 'POST',
897+ headers: getRequestHeaders(),
898+ body: JSON.stringify({
899+ extensionName,
900+ source,
901+ destination,
902+ }),
903+ });
904+
905+ if (!result.ok) {
906+ const text = await result.text();
907+ toastr.error(text || result.statusText, t`Extension move failed`, { timeOut: 5000 });
908+ console.error('Extension move failed', result.status, result.statusText, text);
909+ return;
910+ }
911+
912+ toastr.success(t`Extension ${extensionName} moved.`);
913+ await loadExtensionSettings({}, false, false);
914+ void showExtensionsDetails();
915+ } catch (error) {
916+ console.error('Error:', error);
917+ }
918+}
919+
920+/**
921+ * Deletes an extension via the API.
922+ * @param {string} extensionName Extension name to delete
923+ */
767924export async function deleteExtension(extensionName) {
768925 try {
769926 await fetch('/api/extensions/delete', {
770927 method: 'POST',
771928 headers: getRequestHeaders(),
772929 body: JSON.stringify({ extensionName }),
930+ extensionName,
931+ global: getExtensionType(extensionName) === 'global',
932+ }),
773933 });
774934 } catch (error) {
775935 console.error('Error:', error);
776936 }
777937
778938 toastr.success(t`Extension ${extensionName} deleted`);
779- showExtensionsDetails();
939+ delay(1000).then(() => location.reload());
780- // reload the page to remove the extension from the list
781- location.reload();
782940}
783941
784942/**
785943 * Fetches the version details of a specific extension.
786944 *
787945 * @param {string} extensionName - The name of the extension.
946+ * @param {AbortSignal} [abortSignal] - The signal to abort the operation.
788947 * @return {Promise<object>} - An object containing the extension's version details.
789948 * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl.
790949 * @throws {error} - If there is an error during the fetch operation, it logs the error to the console.
791950 */
792951async function getExtensionVersion(extensionName, abortSignal) {
793952 try {
794953 const response = await fetch('/api/extensions/version', {
795954 method: 'POST',
796955 headers: getRequestHeaders(),
797956 body: JSON.stringify({ extensionName }),
957+ extensionName,
958+ global: getExtensionType(extensionName) === 'global',
959+ }),
960+ signal: abortSignal,
798961 });
799962
800963 const data = await response.json();
@@ -807,22 +970,26 @@ async function getExtensionVersion(extensionName) {
807970/**
808971 * Installs a third-party extension via the API.
809972 * @param {string} url Extension repository URL
973+ * @param {boolean} global Is the extension global?
810974 * @returns {Promise<void>}
811975 */
812976export async function installExtension(url, global) {
813977 console.debug('Extension installation started', url);
814978
815979 toastr.info('t`Please wait...'`, 't`Installing extension'`);
816980
817981 const request = await fetch('/api/extensions/install', {
818982 method: 'POST',
819983 headers: getRequestHeaders(),
820984 body: JSON.stringify({ url }),
985+ url,
986+ global,
987+ }),
821988 });
822989
823990 if (!request.ok) {
824991 const text = await request.text();
825992 toastr.warning(text || request.statusText, 't`Extension installation failed'`, { timeOut: 5000 });
826993 console.error('Extension installation failed', request.status, request.statusText, text);
827994 return;
828995 }
@@ -840,7 +1007,7 @@ export async function installExtension(url) {
8401007 * @param {boolean} versionChanged Is this a version change?
8411008 * @param {boolean} enableAutoUpdate Enable auto-update
8421009 */
8431010export async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) {
8441011 if (settings.extension_settings) {
8451012 Object.assign(extension_settings, settings.extension_settings);
8461013 }
@@ -852,7 +1019,9 @@ async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate)
8521019
8531020 // Activate offline extensions
8541021 await eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
8551022 extensionNamesconst extensions = await discoverExtensions();
1023+ extensionNames = extensions.map(x => x.name);
1024+ extensionTypes = Object.fromEntries(extensions.map(x => [x.name, x.type]));
8561025 manifests = await getManifests(extensionNames);
8571026
8581027 if (versionChanged && enableAutoUpdate) {
@@ -873,6 +1042,86 @@ export function doDailyExtensionUpdatesCheck() {
8731042 }, 1);
8741043}
8751044
1045+const concurrencyLimit = 5;
1046+let activeRequestsCount = 0;
1047+const versionCheckQueue = [];
1048+
1049+function enqueueVersionCheck(fn) {
1050+ return new Promise((resolve, reject) => {
1051+ versionCheckQueue.push(() => fn().then(resolve).catch(reject));
1052+ processVersionCheckQueue();
1053+ });
1054+}
1055+
1056+function processVersionCheckQueue() {
1057+ if (activeRequestsCount >= concurrencyLimit || versionCheckQueue.length === 0) {
1058+ return;
1059+ }
1060+ activeRequestsCount++;
1061+ const fn = versionCheckQueue.shift();
1062+ fn().finally(() => {
1063+ activeRequestsCount--;
1064+ processVersionCheckQueue();
1065+ });
1066+}
1067+
1068+/**
1069+ * Performs a manual check for updates on all 3rd-party extensions.
1070+ * @param {AbortSignal} abortSignal Signal to abort the operation
1071+ * @returns {Promise<any[]>}
1072+ */
1073+async function checkForUpdatesManual(abortSignal) {
1074+ const promises = [];
1075+ for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortManifests(manifests[a], manifests[b]))) {
1076+ const externalId = id.replace('third-party', '');
1077+ const promise = enqueueVersionCheck(async () => {
1078+ try {
1079+ const data = await getExtensionVersion(externalId, abortSignal);
1080+ const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`);
1081+ if (extensionBlock && data) {
1082+ if (data.isUpToDate === false) {
1083+ const buttonElement = extensionBlock.querySelector('.btn_update');
1084+ if (buttonElement) {
1085+ buttonElement.classList.remove('displayNone');
1086+ }
1087+ const nameElement = extensionBlock.querySelector('.extension_name');
1088+ if (nameElement) {
1089+ nameElement.classList.add('update_available');
1090+ }
1091+ }
1092+ let branch = data.currentBranchName;
1093+ let commitHash = data.currentCommitHash;
1094+ let origin = data.remoteUrl;
1095+
1096+ const originLink = extensionBlock.querySelector('a');
1097+ if (originLink) {
1098+ try {
1099+ const url = new URL(origin);
1100+ if (!['https:', 'http:'].includes(url.protocol)) {
1101+ throw new Error('Invalid protocol');
1102+ }
1103+ originLink.href = url.href;
1104+ originLink.target = '_blank';
1105+ originLink.rel = 'noopener noreferrer';
1106+ } catch (error) {
1107+ console.log('Error setting origin link', originLink, error);
1108+ }
1109+ }
1110+
1111+ const versionElement = extensionBlock.querySelector('.extension_version');
1112+ if (versionElement) {
1113+ versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`;
1114+ }
1115+ }
1116+ } catch (error) {
1117+ console.error('Error checking for extension updates', error);
1118+ }
1119+ });
1120+ promises.push(promise);
1121+ }
1122+ return Promise.allSettled(promises);
1123+}
1124+
8761125/**
8771126 * Checks if there are updates available for 3rd-party extensions.
8781127 * @param {boolean} force Skip nag check
@@ -891,21 +1140,26 @@ async function checkForExtensionUpdates(force) {
8911140 localStorage.setItem(STORAGE_NAG_KEY, currentDate);
8921141 }
8931142
1143+ const isCurrentUserAdmin = isAdmin();
8941144 const updatesAvailable = [];
8951145 const promises = [];
8961146
8971147 for (const [id, manifest] of Object.entries(manifests)) {
1148+ const isGlobal = getExtensionType(id) === 'global';
1149+ if (isGlobal && !isCurrentUserAdmin) {
1150+ console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`);
1151+ continue;
1152+ }
1153+
8981154 if (manifest.auto_update && id.startsWith('third-party')) {
8991155 const promise = new PromiseenqueueVersionCheck(async (resolve, reject) => {
9001156 try {
9011157 const data = await getExtensionVersion(id.replace('third-party', ''));
9021158 if (!data.isUpToDate === false) {
9031159 updatesAvailable.push(manifest.display_name);
9041160 }
905- resolve();
9061161 } catch (error) {
9071162 console.error('Error checking for extension updates', error);
908- reject();
9091163 }
9101164 });
9111165 promises.push(promise);
@@ -930,8 +1184,14 @@ async function autoUpdateExtensions(forceAll) {
9301184 }
9311185
9321186 const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 });
1187+ const isCurrentUserAdmin = isAdmin();
9331188 const promises = [];
9341189 for (const [id, manifest] of Object.entries(manifests)) {
1190+ const isGlobal = getExtensionType(id) === 'global';
1191+ if (isGlobal && !isCurrentUserAdmin) {
1192+ console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`);
1193+ continue;
1194+ }
9351195 if ((forceAll || manifest.auto_update) && id.startsWith('third-party')) {
9361196 console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`);
9371197 promises.push(updateExtension(id.replace('third-party', ''), true));
@@ -947,7 +1207,7 @@ async function autoUpdateExtensions(forceAll) {
9471207 * @param {number} contextSize Context size
9481208 * @returns {Promise<boolean>} True if generation should be aborted
9491209 */
9501210export async function runGenerationInterceptors(chat, contextSize) {
9511211 let aborted = false;
9521212 let exitImmediately = false;
9531213
@@ -956,11 +1216,11 @@ async function runGenerationInterceptors(chat, contextSize) {
9561216 exitImmediately = immediately;
9571217 };
9581218
9591219 for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifests(a.loading_order -, b.loading_order))) {
9601220 const interceptorKey = manifest.generate_interceptor;
9611221 if (typeof windowglobalThis[interceptorKey] === 'function') {
9621222 try {
9631223 await windowglobalThis[interceptorKey](chat, contextSize, abort);
9641224 } catch (e) {
9651225 console.error(`Failed running interceptor for ${manifest.display_name}`, e);
9661226 }
@@ -1033,8 +1293,23 @@ export async function writeExtensionField(characterId, key, value) {
10331293 * @returns {Promise<void>}
10341294 */
10351295export async function openThirdPartyExtensionMenu(suggestUrl = '') {
10361296 const htmlisCurrentUserAdmin = await renderTemplateAsyncisAdmin('installExtension');
10371297 const inputhtml = await callGenericPopuprenderTemplateAsync(html, POPUP_TYPE.INPUT'installExtension', suggestUrl{ ??isCurrentUserAdmin ''});
1298+ const okButton = isCurrentUserAdmin ? t`Install just for me` : t`Install`;
1299+
1300+ let global = false;
1301+ const installForAllButton = {
1302+ text: t`Install for all users`,
1303+ appendAtEnd: false,
1304+ action: async () => {
1305+ global = true;
1306+ await popup.complete(POPUP_RESULT.AFFIRMATIVE);
1307+ },
1308+ };
1309+
1310+ const customButtons = isCurrentUserAdmin ? [installForAllButton] : [];
1311+ const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons });
1312+ const input = await popup.show();
10381313
10391314 if (!input) {
10401315 console.debug('Extension install cancelled');
@@ -1042,11 +1317,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
10421317 }
10431318
10441319 const url = String(input).trim();
10451320 await installExtension(url, global);
10461321}
10471322
1048-
1049-
10501323export async function initExtensions() {
10511324 await addExtensionsButtonAndMenu();
10521325 $('#extensionsMenuButton').css('display', 'flex');
@@ -1055,10 +1328,11 @@ export async function initExtensions() {
10551328 $('#extensions_autoconnect').on('input', autoConnectInputHandler);
10561329 $('#extensions_details').on('click', showExtensionsDetails);
10571330 $('#extensions_notify_updates').on('input', notifyUpdatesInputHandler);
10581331 $(document).on('click', '.extensions_info .extension_block .toggle_disable', onDisableExtensionClick);
10591332 $(document).on('click', '.extensions_info .extension_block .toggle_enable', onEnableExtensionClick);
10601333 $(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
10611334 $(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
1335+ $(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
10621336
10631337 /**
10641338 * Handles the click event for the third-party extension import button.
public/scripts/extensions/caption/settings.html+2 -0
@@ -53,6 +53,7 @@
5353 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
5454 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
5555 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
56+ <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
5657 <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
5758 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
5859 <option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
@@ -62,6 +63,7 @@
6263 <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
6364 <option data-type="google" value="gemini-exp-1114">gemini-exp-1114</option>
6465 <option data-type="google" value="gemini-exp-1121">gemini-exp-1121</option>
66+ <option data-type="google" value="gemini-exp-1206">gemini-exp-1206</option>
6567 <option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
6668 <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
6769 <option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
public/scripts/extensions/expressions/index.js+10 -0
@@ -697,6 +697,11 @@ async function moduleWorker() {
697697 return;
698698 }
699699
700+ // If using LLM api then check if streamingProcessor is finished to avoid sending multiple requests to the API
701+ if (extension_settings.expressions.api === EXPRESSION_API.llm && context.streamingProcessor && !context.streamingProcessor.isFinished) {
702+ return;
703+ }
704+
700705 // API is busy
701706 if (inApiCall) {
702707 console.debug('Classification API is busy');
@@ -995,6 +1000,11 @@ function sampleClassifyText(text) {
9951000 // Replace macros, remove asterisks and quotes
9961001 let result = substituteParams(text).replace(/[*"]/g, '');
9971002
1003+ // If using LLM api there is no need to check length of characters
1004+ if (extension_settings.expressions.api === EXPRESSION_API.llm) {
1005+ return result.trim();
1006+ }
1007+
9981008 const SAMPLE_THRESHOLD = 500;
9991009 const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
10001010
public/scripts/extensions/stable-diffusion/index.js+0 -15
@@ -60,7 +60,6 @@ import { ToolManager } from '../../tool-calling.js';
6060export { MODULE_NAME };
6161
6262const MODULE_NAME = 'sd';
63-const UPDATE_INTERVAL = 1000;
6463// This is a 1x1 transparent PNG
6564const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
6665const CUSTOM_STOP_EVENT = 'sd_stop_generation';
@@ -3687,8 +3686,6 @@ async function addSDGenButtons() {
36873686 const button = $('#sd_gen');
36883687 const dropdown = $('#sd_dropdown');
36893688 dropdown.hide();
3690- button.hide();
3691- messageButton.hide();
36923689
36933690 let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
36943691 placement: 'top',
@@ -3770,18 +3767,6 @@ function isValidState() {
37703767 }
37713768}
37723769
3773-async function moduleWorker() {
3774- if (isValidState()) {
3775- $('#sd_gen').show();
3776- $('.sd_message_gen').show();
3777- }
3778- else {
3779- $('#sd_gen').hide();
3780- $('.sd_message_gen').hide();
3781- }
3782-}
3783-
3784-setInterval(moduleWorker, UPDATE_INTERVAL);
37853770let buttonAbortController = null;
37863771
37873772async function sdMessageButton(e) {
public/scripts/extensions/third-party/.gitkeep+0 -0
public/scripts/extensions/tts/alltalk.js+1072 -1072
@@ -1,1072 +1,1072 @@
11import { doExtrasFetch } from '../../extensions.js';
22import { debounce } from '../../utils.js';
33import { saveTtsProviderSettings } from './index.js';
4-
4+
55export { AllTalkTtsProvider };
6-
6+
77class AllTalkTtsProvider {
88 //########//
99 // Config //
1010 //########//
11-
11+
1212 settings = {};
1313 constructor() {
1414 // Initialize with default settings if they are not already set
1515 this.settings = {
1616 provider_endpoint: this.settings.provider_endpoint || 'http://localhost:7851',
1717 server_version: this.settings.server_version || 'v2',
1818 language: this.settings.language || 'en',
1919 voiceMap: this.settings.voiceMap || {},
2020 at_generation_method: this.settings.at_generation_method || 'standard_generation',
2121 narrator_enabled: this.settings.narrator_enabled || 'false',
2222 at_narrator_text_not_inside: this.settings.at_narrator_text_not_inside || 'narrator',
2323 narrator_voice_gen: this.settings.narrator_voice_gen || 'Please set a voice',
2424 rvc_character_voice: this.settings.rvc_character_voice || 'Disabled',
2525 rvc_character_pitch: this.settings.rvc_character_pitch || '0',
2626 rvc_narrator_voice: this.settings.rvc_narrator_voice || 'Disabled',
2727 rvc_narrator_pitch: this.settings.rvc_narrator_pitch || '0',
2828 finetuned_model: this.settings.finetuned_model || 'false',
2929 };
3030 // Separate property for dynamically updated settings from the server
3131 this.dynamicSettings = {
3232 modelsAvailable: [],
3333 currentModel: '',
3434 deepspeed_available: false,
3535 deepspeed_enabled: false,
3636 lowvram_capable: false,
3737 lowvram_enabled: false,
3838 };
3939 this.rvcVoices = []; // Initialize rvcVoices as an empty array
4040 }
4141 ready = false;
4242 voices = [];
4343 separator = '. ';
4444 audioElement = document.createElement('audio');
45-
45+
4646 languageLabels = {
4747 'Arabic': 'ar',
4848 'Brazilian Portuguese': 'pt',
4949 'Chinese': 'zh-cn',
5050 'Czech': 'cs',
5151 'Dutch': 'nl',
5252 'English': 'en',
5353 'French': 'fr',
5454 'German': 'de',
5555 'Italian': 'it',
5656 'Polish': 'pl',
5757 'Russian': 'ru',
5858 'Spanish': 'es',
5959 'Turkish': 'tr',
6060 'Japanese': 'ja',
6161 'Korean': 'ko',
6262 'Hungarian': 'hu',
6363 'Hindi': 'hi',
6464 };
65-
65+
6666 get settingsHtml() {
6767 // HTML template literals can trigger ESLint quotes warnings when quotes are used in HTML attributes.
6868 // Disabling quotes rule for this one line as it's a false positive with HTML template literals.
6969 // eslint-disable-next-line quotes
7070 let html = `<div class="at-settings-separator">AllTalk V2 Settings</div>`;
71-
71+
7272 html += `<div class='at-settings-row'>
7373 <div class='at-settings-option'>
7474 <label for='at_generation_method'>AllTalk TTS Generation Method</label>
7575 <select id='at_generation_method'>
7676 <option value='standard_generation'>Standard Audio Generation (AT Narrator - Optional)</option>
7777 <option value='streaming_enabled'>Streaming Audio Generation (AT Narrator - Disabled)</option>
7878 </select>
7979 </div>
8080 </div>`;
81-
81+
8282 html += `<div class='at-settings-row'>
8383 <div class='at-settings-option'>
8484 <label for='at_narrator_enabled'>AT Narrator</label>
8585 <select id='at_narrator_enabled'>
8686 <option value='true'>Enabled</option>
8787 <option value='silent'>Enabled (Silenced)</option>
8888 <option value='false'>Disabled</option>
8989 </select>
9090 </div>
91-
91+
9292 <div class='at-settings-option'>
9393 <label for='at_narrator_text_not_inside'>Text Not Inside * or " is</label>
9494 <select id='at_narrator_text_not_inside'>
9595 <option value='character'>Character</option>
9696 <option value='narrator'>Narrator</option>
9797 <option value='silent'>Silent</option>
9898 </select>
9999 </div>
100100 </div>`;
101-
101+
102102 html += `<div class='at-settings-row'>
103103 <div class='at-settings-option'>
104104 <label for='narrator_voice'>Narrator Voice</label>
105105 <select id='narrator_voice'>`;
106106 if (this.voices) {
107107 for (let voice of this.voices) {
108108 html += `<option value='${voice.voice_id}'>${voice.name}</option>`;
109109 }
110110 }
111111 html += `</select>
112112 </div>
113113 <div class='at-settings-option'>
114114 <label for='language_options'>Language</label>
115115 <select id='language_options'>`;
116116 for (let language in this.languageLabels) {
117117 html += `<option value='${this.languageLabels[language]}' ${this.languageLabels[language] === this.settings?.language ? 'selected="selected"' : ''}>${language}</option>`;
118118 }
119119 html += `</select>
120120 </div>
121121 </div>`;
122-
122+
123123 html += `<div class='at-settings-row'>
124124 <div class='at-settings-option'>
125125 <label for='rvc_character_voice'>RVC Character</label>
126126 <select id='rvc_character_voice'>`;
127127 if (this.rvcVoices) {
128128 for (let rvccharvoice of this.rvcVoices) {
129129 html += `<option value='${rvccharvoice.voice_id}'>${rvccharvoice.name}</option>`;
130130 }
131131 }
132132 html += `</select>
133133 </div>
134134 <div class='at-settings-option'>
135135 <label for='rvc_narrator_voice'>RVC Narrator</label>
136136 <select id='rvc_narrator_voice'>`;
137137 if (this.rvcVoices) {
138138 for (let rvcnarrvoice of this.rvcVoices) {
139139 html += `<option value='${rvcnarrvoice.voice_id}'>${rvcnarrvoice.name}</option>`;
140140 }
141141 }
142142 html += `</select>
143143 </div>
144144</div>`;
145-
145+
146146 html += `<div class='at-settings-row'>
147147 <div class='at-settings-option'>
148148 <label for='rvc_character_pitch'>RVC Character Pitch</label>
149149 <select id='rvc_character_pitch'>`;
150150 for (let i = -24; i <= 24; i++) {
151151 const selected = i === 0 ? 'selected="selected"' : '';
152152 html += `<option value='${i}' ${selected}>${i}</option>`;
153153 }
154154 html += `</select>
155155 </div>
156156 <div class='at-settings-option'>
157157 <label for='rvc_narrator_pitch'>RVC Narrator Pitch</label>
158158 <select id='rvc_narrator_pitch'>`;
159159 for (let i = -24; i <= 24; i++) {
160160 const selected = i === 0 ? 'selected="selected"' : '';
161161 html += `<option value='${i}' ${selected}>${i}</option>`;
162162 }
163163 html += `</select>
164164 </div>
165165 </div>`;
166-
166+
167167 html += `<div class='at-model-endpoint-row'>
168168 <div class='at-model-option'>
169169 <label for='switch_model'>Switch Model</label>
170170 <select id='switch_model'>
171171 <!-- Options will be dynamically populated -->
172172 </select>
173173 </div>
174-
174+
175175 <div class='at-endpoint-option'>
176176 <label for='at_server'>AllTalk Endpoint:</label>
177177 <input id='at_server' type='text' class='text_pole' maxlength='80' value='${this.settings.provider_endpoint}'/>
178178 </div>
179179 </div>`;
180-
180+
181181 html += `<div class='at-settings-row'>
182182 <div class='at-settings-option'>
183183 <label for='server_version'>AllTalk Server Version</label>
184184 <select id='server_version'>
185185 <option value='v1'>AllTalk V1</option>
186186 <option value='v2'>AllTalk V2</option>
187187 </select>
188188 </div>
189189 </div>`;
190-
190+
191191 html += `<div class='at-model-endpoint-row'>
192192 <div class='at-settings-option'>
193193 <label for='low_vram'>Low VRAM</label>
194194 <input id='low_vram' type='checkbox'/>
195195 </div>
196196 <div class='at-settings-option'>
197197 <label for='deepspeed'>DeepSpeed</label>
198198 <input id='deepspeed' type='checkbox'/>
199199 </div>
200200 <div class='at-settings-option status-option'>
201201 <span>Status: <span id='status_info'>Ready</span></span>
202202 </div>
203203 <div class='at-settings-option empty-option'>
204204 <!-- This div remains empty for spacing -->
205205 </div>
206206</div>`;
207-
207+
208208 html += `<div class='at-website-row'>
209209 <div class='at-website-option'>
210210 <span>AllTalk V2<a target='_blank' href='${this.settings.provider_endpoint}'>Config & Docs</a>.</span>
211211 </div>
212-
212+
213213 <div class='at-website-option'>
214214 <span>AllTalk <a target='_blank' href='https://github.com/erew123/alltalk_tts/'>Website</a>.</span>
215215 </div>
216216</div>`;
217-
217+
218218 html += `<div class='at-website-row'>
219219<div class='at-website-option'>
220220<span>- If you <strong>change your TTS engine</strong> in AllTalk, you will need to <strong>Reload</strong> (button above) and re-select your voices.</span><br><br>
221221<span>- Assuming the server is <strong>Status: Ready</strong>, most problems will be resolved by hitting Reload and selecting voices that match the loaded TTS engine.</span><br><br>
222222<span>- <strong>Text-generation-webui</strong> users - Uncheck <strong>Enable TTS</strong> in the TGWUI interface, or you will hear 2x voices and file names being generated.</span>
223223</div>
224224</div>`;
225-
225+
226226 return html;
227227 }
228-
228+
229-
229+
230230 //#################//
231231 // Startup ST & AT //
232232 //#################//
233-
233+
234234 async loadSettings(settings) {
235235 updateStatus('Offline');
236-
236+
237237 if (Object.keys(settings).length === 0) {
238238 console.info('Using default AllTalk TTS Provider settings');
239239 } else {
240240 // Populate settings with provided values, ignoring server-provided settings
241241 for (const key in settings) {
242242 if (key in this.settings) {
243243 this.settings[key] = settings[key];
244244 } else {
245245 console.debug(`Ignoring non-user-configurable setting: ${key}`);
246246 }
247247 }
248248 }
249-
249+
250250 // Update UI elements to reflect the loaded settings
251251 $('#at_server').val(this.settings.provider_endpoint);
252252 $('#language_options').val(this.settings.language);
253253 $('#at_generation_method').val(this.settings.at_generation_method);
254254 $('#at_narrator_enabled').val(this.settings.narrator_enabled);
255255 $('#at_narrator_text_not_inside').val(this.settings.at_narrator_text_not_inside);
256256 $('#narrator_voice').val(this.settings.narrator_voice_gen);
257257 $('#rvc_character_voice').val(this.settings.rvc_character_voice);
258258 $('#rvc_narrator_voice').val(this.settings.rvc_narrator_voice);
259259 $('#rvc_character_pitch').val(this.settings.rvc_character_pitch);
260260 $('#rvc_narrator_pitch').val(this.settings.rvc_narrator_pitch);
261-
261+
262262 console.debug('AllTalkTTS: Settings loaded');
263263 try {
264264 // Check if TTS provider is ready
265265 await this.checkReadysetupEventListeners();
266- await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server
266+ this.updateLanguageDropdown();
267- await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready
267+ await this.checkReady();
268268 await this.fetchRvcVoiceObjectsupdateSettingsFromServer(); // Fetch RVCdynamic voices settings from the TTS server
269- this.updateNarratorVoicesDropdown();
269+ await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready
270- this.updateLanguageDropdown();
270+ await this.fetchRvcVoiceObjects(); // Fetch RVC voices
271271 this.setupEventListenersupdateNarratorVoicesDropdown();
272272 this.applySettingsToHTML();
273273 updateStatus('Ready');
274274 } catch (error) {
275275 console.error('Error loading settings:', error);
276276 updateStatus('Offline');
277277 }
278278 }
279-
279+
280-
280+
281281 applySettingsToHTML() {
282282 const narratorVoiceSelect = document.getElementById('narrator_voice');
283283 const atNarratorSelect = document.getElementById('at_narrator_enabled');
284284 const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside');
285285 const generationMethodSelect = document.getElementById('at_generation_method');
286286 this.settings.narrator_voice = this.settings.narrator_voice_gen;
287287 // Apply settings to Narrator Voice dropdown
288288 if (narratorVoiceSelect && this.settings.narrator_voice) {
289289 narratorVoiceSelect.value = this.settings.narrator_voice; // Remove the parentheses
290290 }
291291 // Apply settings to AT Narrator Enabled dropdown
292292 if (atNarratorSelect) {
293293 const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks');
294294 const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted');
295295 const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues');
296296 if (this.settings.narrator_enabled) {
297297 ttsPassAsterisksCheckbox.checked = false;
298298 $('#tts_pass_asterisks').click();
299299 $('#tts_pass_asterisks').trigger('change');
300300 }
301301 if (!this.settings.narrator_enabled) {
302302 ttsPassAsterisksCheckbox.checked = true;
303303 $('#tts_pass_asterisks').click();
304304 $('#tts_pass_asterisks').trigger('change');
305305 }
306306 if (this.settings.narrator_enabled) {
307307 ttsNarrateQuotedCheckbox.checked = true;
308308 ttsNarrateDialoguesCheckbox.checked = true;
309309 $('#tts_narrate_quoted').click();
310310 $('#tts_narrate_quoted').trigger('change');
311311 $('#tts_narrate_dialogues').click();
312312 $('#tts_narrate_dialogues').trigger('change');
313313 }
314314 atNarratorSelect.value = this.settings.narrator_enabled.toString();
315315 this.settings.narrator_enabled = this.settings.narrator_enabled.toString();
316316 }
317317 const languageSelect = document.getElementById('language_options');
318318 if (languageSelect && this.settings.language) {
319319 languageSelect.value = this.settings.language;
320320 }
321321 if (textNotInsideSelect && this.settings.text_not_inside) {
322322 textNotInsideSelect.value = this.settings.text_not_inside;
323323 this.settings.at_narrator_text_not_inside = this.settings.text_not_inside;
324324 }
325325 if (generationMethodSelect && this.settings.at_generation_method) {
326326 generationMethodSelect.value = this.settings.at_generation_method;
327327 }
328328 const isStreamingEnabled = this.settings.at_generation_method === 'streaming_enabled';
329329 if (isStreamingEnabled) {
330330 if (atNarratorSelect) atNarratorSelect.disabled = true;
331331 if (textNotInsideSelect) textNotInsideSelect.disabled = true;
332332 if (narratorVoiceSelect) narratorVoiceSelect.disabled = true;
333333 } else {
334334 if (atNarratorSelect) atNarratorSelect.disabled = false;
335335 if (textNotInsideSelect) textNotInsideSelect.disabled = !this.settings.narrator_enabled;
336336 if (narratorVoiceSelect) narratorVoiceSelect.disabled = !this.settings.narrator_enabled;
337337 }
338338 }
339-
339+
340-
340+
341341 //##############################//
342342 // Check AT Server is Available //
343343 //##############################//
344-
344+
345345 async checkReady() {
346346 try {
347347 const response = await fetch(`${this.settings.provider_endpoint}/api/ready`);
348348 // Check if the HTTP request was successful
349349 if (!response.ok) {
350350 throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
351351 }
352352 const statusText = await response.text();
353353 // Check if the response is 'Ready'
354354 if (statusText === 'Ready') {
355355 this.ready = true; // Set the ready flag to true
356356 console.log('TTS service is ready.');
357357 } else {
358358 this.ready = false;
359359 console.log('TTS service is not ready.');
360360 }
361361 } catch (error) {
362362 console.error('Error checking TTS service readiness:', error);
363363 this.ready = false; // Ensure ready flag is set to false in case of error
364364 }
365365 }
366-
366+
367367 //######################//
368368 // Get Available Voices //
369369 //######################//
370-
370+
371371 async fetchTtsVoiceObjects() {
372372 const response = await fetch(`${this.settings.provider_endpoint}/api/voices`);
373373 if (!response.ok) {
374374 const errorText = await response.text();
375375 throw new Error(`HTTP ${response.status}: ${errorText}`);
376376 }
377377 const data = await response.json();
378378 const voices = data.voices.map(filename => {
379379 return {
380380 name: filename,
381381 voice_id: filename,
382382 preview_url: null, // Preview URL will be dynamically generated
383383 lang: 'en', // Default language
384384 };
385385 });
386386 this.voices = voices; // Assign to the class property
387387 return voices; // Also return this list
388388 }
389-
389+
390390 async fetchRvcVoiceObjects() {
391391 if (this.settings.server_version !== 'v2') {
392392 console.log('Skipping RVC voices fetch for V1 server');
393393 return [];
394394 }
395-
395+
396396 console.log('Fetching RVC Voices');
397397 try {
398398 const response = await fetch(`${this.settings.provider_endpoint}/api/rvcvoices`);
399399 if (!response.ok) {
400400 const errorText = await response.text();
401401 console.error('Error text:', errorText);
402402 throw new Error(`HTTP ${response.status}: ${errorText}`);
403403 }
404-
404+
405405 const data = await response.json();
406406 if (!data || !data.rvcvoices) {
407407 console.error('Invalid data format:', data);
408408 throw new Error('Invalid data format received from /api/rvcvoices');
409409 }
410-
410+
411411 const voices = data.rvcvoices.map(filename => {
412412 return {
413413 name: filename,
414414 voice_id: filename,
415415 };
416416 });
417-
417+
418418 console.log('RVC voices:', voices);
419419 this.rvcVoices = voices; // Assign to the class property
420420 this.updateRvcVoiceDropdowns(); // Update UI after fetching voices
421421 return voices; // Also return this list
422422 } catch (error) {
423423 console.error('Error fetching RVC voices:', error);
424424 this.rvcVoices = [{ name: 'Disabled', voice_id: 'Disabled' }]; // Set default on error
425425 throw error;
426426 } finally {
427427 // Ensure dropdowns are updated even if there was an error
428428 this.updateRvcVoiceDropdowns();
429429 }
430430 }
431-
431+
432432 //##########################################//
433433 // Get Current AT Server Config & Update ST //
434434 //##########################################//
435-
435+
436436 async updateSettingsFromServer() {
437437 try {
438438 const response = await fetch(`${this.settings.provider_endpoint}/api/currentsettings`);
439439 if (!response.ok) {
440440 throw new Error(`Failed to fetch current settings: ${response.statusText}`);
441441 }
442442 const currentSettings = await response.json();
443443 currentSettings.models_available.sort((a, b) => a.name.localeCompare(b.name));
444-
444+
445445 this.settings.enginesAvailable = currentSettings.engines_available;
446446 this.settings.currentEngineLoaded = currentSettings.current_engine_loaded;
447447 this.settings.modelsAvailable = currentSettings.models_available;
448448 this.settings.currentModel = currentSettings.current_model_loaded;
449449 this.settings.deepspeed_capable = currentSettings.deepspeed_capable;
450450 this.settings.deepspeed_available = currentSettings.deepspeed_available;
451451 this.settings.deepspeed_enabled = currentSettings.deepspeed_enabled;
452452 this.settings.lowvram_capable = currentSettings.lowvram_capable;
453453 this.settings.lowvram_enabled = currentSettings.lowvram_enabled;
454-
454+
455455 await this.fetchRvcVoiceObjects(); // Fetch RVC voices
456-
456+
457457 this.updateModelDropdown();
458458 this.updateCheckboxes();
459459 this.updateRvcVoiceDropdowns(); // Update the RVC voice dropdowns
460460 } catch (error) {
461461 console.error(`Error updating settings from server: ${error}`);
462462 }
463463 }
464-
464+
465465 updateRvcVoiceDropdowns() {
466466 // Handle all RVC-related elements
467467 const rvcElements = document.querySelectorAll('.rvc-setting');
468468 const isV2 = this.settings.server_version === 'v2';
469-
469+
470470 rvcElements.forEach(element => {
471471 element.style.display = isV2 ? 'block' : 'none';
472472 });
473-
473+
474474 // Update and disable/enable character voice dropdown
475475 const rvcCharacterVoiceSelect = document.getElementById('rvc_character_voice');
476476 if (rvcCharacterVoiceSelect) {
477477 rvcCharacterVoiceSelect.disabled = !isV2;
478478 if (this.rvcVoices) {
479479 rvcCharacterVoiceSelect.innerHTML = '';
480480 for (let voice of this.rvcVoices) {
481481 const option = document.createElement('option');
482482 option.value = voice.voice_id;
483483 option.textContent = voice.name;
484484 if (voice.voice_id === this.settings.rvc_character_voice) {
485485 option.selected = true;
486486 }
487487 rvcCharacterVoiceSelect.appendChild(option);
488488 }
489489 }
490490 }
491-
491+
492492 // Update and disable/enable narrator voice dropdown
493493 const rvcNarratorVoiceSelect = document.getElementById('rvc_narrator_voice');
494494 if (rvcNarratorVoiceSelect) {
495495 rvcNarratorVoiceSelect.disabled = !isV2;
496496 if (this.rvcVoices) {
497497 rvcNarratorVoiceSelect.innerHTML = '';
498498 for (let voice of this.rvcVoices) {
499499 const option = document.createElement('option');
500500 option.value = voice.voice_id;
501501 option.textContent = voice.name;
502502 if (voice.voice_id === this.settings.rvc_narrator_voice) {
503503 option.selected = true;
504504 }
505505 rvcNarratorVoiceSelect.appendChild(option);
506506 }
507507 }
508508 }
509-
509+
510510 // Update pitch inputs
511511 const characterPitch = document.getElementById('rvc_character_pitch');
512512 if (characterPitch) {
513513 characterPitch.disabled = !isV2;
514514 }
515-
515+
516516 const narratorPitch = document.getElementById('rvc_narrator_pitch');
517517 if (narratorPitch) {
518518 narratorPitch.disabled = !isV2;
519519 }
520520 }
521-
521+
522522 //###################################################//
523523 // Get Current AT Server Config & Update ST (Models) //
524524 //###################################################//
525-
525+
526526 updateModelDropdown() {
527527 const modelSelect = document.getElementById('switch_model');
528528 if (modelSelect) {
529529 modelSelect.innerHTML = ''; // Clear existing options
530530 this.settings.modelsAvailable.forEach(model => {
531531 const option = document.createElement('option');
532532 option.value = model.name;
533533 option.textContent = model.name; // Use model name directly
534534 option.selected = model.name === this.settings.currentModel;
535535 modelSelect.appendChild(option);
536536 });
537537 }
538538 }
539-
539+
540540 //#######################################################//
541541 // Get Current AT Server Config & Update ST (DS and LVR) //
542542 //#######################################################//
543-
543+
544544 updateCheckboxes() {
545545 const deepspeedCheckbox = document.getElementById('deepspeed');
546546 const lowVramCheckbox = document.getElementById('low_vram');
547-
547+
548548 // Handle DeepSpeed checkbox
549549 if (deepspeedCheckbox) {
550550 if (this.settings.deepspeed_capable) {
551551 // If TTS engine is capable of using DeepSpeed
552552 deepspeedCheckbox.disabled = !this.settings.deepspeed_available;
553553 this.settings.deepspeed_enabled = this.settings.deepspeed_available && this.settings.deepspeed_enabled;
554554 } else {
555555 // If TTS engine is NOT capable of using DeepSpeed
556556 deepspeedCheckbox.disabled = true;
557557 this.settings.deepspeed_enabled = false;
558558 }
559559 deepspeedCheckbox.checked = this.settings.deepspeed_enabled;
560560 }
561-
561+
562562 // Handle Low VRAM checkbox
563563 if (lowVramCheckbox) {
564564 if (this.settings.lowvram_capable) {
565565 // If TTS engine is capable of low VRAM
566566 lowVramCheckbox.disabled = false;
567567 } else {
568568 // If TTS engine is NOT capable of low VRAM
569569 lowVramCheckbox.disabled = true;
570570 this.settings.lowvram_enabled = false;
571571 }
572572 lowVramCheckbox.checked = this.settings.lowvram_enabled;
573573 }
574574 }
575-
575+
576-
576+
577577 //###############################################################//
578578 // Get Current AT Server Config & Update ST (AT Narrator Voices) //
579579 //###############################################################//
580-
580+
581581 updateNarratorVoicesDropdown() {
582582 const narratorVoiceSelect = document.getElementById('narrator_voice');
583583 if (narratorVoiceSelect && this.voices) {
584584 // Clear existing options
585585 narratorVoiceSelect.innerHTML = '';
586586 // Add new options
587587 for (let voice of this.voices) {
588588 const option = document.createElement('option');
589589 option.value = voice.voice_id;
590590 option.textContent = voice.name;
591591 narratorVoiceSelect.appendChild(option);
592592 }
593593 }
594594 }
595-
595+
596596 //######################################################//
597597 // Get Current AT Server Config & Update ST (Languages) //
598598 //######################################################//
599-
599+
600600 updateLanguageDropdown() {
601601 const languageSelect = document.getElementById('language_options');
602602 if (languageSelect) {
603603 // Ensure default language is set
604604 this.settings.language = this.settings.language || 'en';
605-
605+
606606 languageSelect.innerHTML = '';
607607 for (let language in this.languageLabels) {
608608 const option = document.createElement('option');
609609 option.value = this.languageLabels[language];
610610 option.textContent = language;
611611 if (this.languageLabels[language] === this.settings.language) {
612612 option.selected = true;
613613 }
614614 languageSelect.appendChild(option);
615615 }
616616 }
617617 }
618-
618+
619619 //########################################//
620620 // Start AT TTS extenstion page listeners //
621621 //########################################//
622-
622+
623623 setupEventListeners() {
624624 // Define the event handler function
625625 const onModelSelectChange = async (event) => {
626626 console.log('Model select change event triggered');
627627 const selectedModel = event.target.value;
628628 console.log(`Selected model: ${selectedModel}`);
629629 updateStatus('Processing');
630630 try {
631631 const response = await fetch(`${this.settings.provider_endpoint}/api/reload?tts_method=${encodeURIComponent(selectedModel)}`, {
632632 method: 'POST',
633633 });
634634 if (!response.ok) {
635635 throw new Error(`HTTP Error: ${response.status}`);
636636 }
637637 const data = await response.json();
638638 console.log('POST response data:', data);
639639 updateStatus('Ready');
640640 } catch (error) {
641641 console.error('POST request error:', error);
642642 updateStatus('Error');
643643 }
644644 };
645-
645+
646646 // Switch Model Listener with debounce
647647 const modelSelect = document.getElementById('switch_model');
648648 if (modelSelect) {
649649 const debouncedModelSelectChange = debounce(onModelSelectChange, 1400);
650650 modelSelect.addEventListener('change', debouncedModelSelectChange);
651651 }
652-
652+
653653 // AllTalk Server version change listener
654654 const serverVersionSelect = document.getElementById('server_version');
655655 if (serverVersionSelect) {
656656 serverVersionSelect.addEventListener('change', async (event) => {
657657 this.settings.server_version = event.target.value;
658658 if (event.target.value === 'v2') {
659659 await this.fetchRvcVoiceObjects();
660660 }
661661 this.updateRvcVoiceDropdowns();
662662 this.onSettingsChange();
663663 });
664664 }
665-
665+
666666 // RVC Voice and Pitch listeners
667667 const rvcCharacterVoiceSelect = document.getElementById('rvc_character_voice');
668668 if (rvcCharacterVoiceSelect) {
669669 rvcCharacterVoiceSelect.addEventListener('change', (event) => {
670670 this.settings.rvccharacter_voice_gen = event.target.value;
671671 this.onSettingsChange();
672672 });
673673 }
674-
674+
675675 const rvcNarratorVoiceSelect = document.getElementById('rvc_narrator_voice');
676676 if (rvcNarratorVoiceSelect) {
677677 rvcNarratorVoiceSelect.addEventListener('change', (event) => {
678678 this.settings.rvcnarrator_voice_gen = event.target.value;
679679 this.onSettingsChange();
680680 });
681681 }
682-
682+
683683 const rvcCharacterPitchSelect = document.getElementById('rvc_character_pitch');
684684 if (rvcCharacterPitchSelect) {
685685 rvcCharacterPitchSelect.addEventListener('change', (event) => {
686686 this.settings.rvc_character_pitch = event.target.value;
687687 this.onSettingsChange();
688688 });
689689 }
690-
690+
691691 const rvcNarratorPitchSelect = document.getElementById('rvc_narrator_pitch');
692692 if (rvcNarratorPitchSelect) {
693693 rvcNarratorPitchSelect.addEventListener('change', (event) => {
694694 this.settings.rvc_narrator_pitch = event.target.value;
695695 this.onSettingsChange();
696696 });
697697 }
698-
698+
699699 // DeepSpeed Listener
700700 const deepspeedCheckbox = document.getElementById('deepspeed');
701701 if (deepspeedCheckbox) {
702702 const handleDeepSpeedChange = async (event) => {
703703 const deepSpeedValue = event.target.checked ? 'True' : 'False';
704704 updateStatus('Processing');
705705 try {
706706 const response = await fetch(`${this.settings.provider_endpoint}/api/deepspeed?new_deepspeed_value=${deepSpeedValue}`, {
707707 method: 'POST',
708708 });
709709 if (!response.ok) {
710710 throw new Error(`HTTP Error: ${response.status}`);
711711 }
712712 const data = await response.json();
713713 console.log('POST response data:', data);
714714 updateStatus('Ready');
715715 } catch (error) {
716716 console.error('POST request error:', error);
717717 updateStatus('Error');
718718 }
719719 };
720-
720+
721721 const debouncedHandleDeepSpeedChange = debounce(handleDeepSpeedChange, 300);
722722 deepspeedCheckbox.addEventListener('change', debouncedHandleDeepSpeedChange);
723723 }
724-
724+
725725 // Low VRAM Listener
726726 const lowVramCheckbox = document.getElementById('low_vram');
727727 if (lowVramCheckbox) {
728728 const handleLowVramChange = async (event) => {
729729 const lowVramValue = event.target.checked ? 'True' : 'False';
730730 updateStatus('Processing');
731731 try {
732732 const response = await fetch(`${this.settings.provider_endpoint}/api/lowvramsetting?new_low_vram_value=${lowVramValue}`, {
733733 method: 'POST',
734734 });
735735 if (!response.ok) {
736736 throw new Error(`HTTP Error: ${response.status}`);
737737 }
738738 const data = await response.json();
739739 console.log('POST response data:', data);
740740 updateStatus('Ready');
741741 } catch (error) {
742742 console.error('POST request error:', error);
743743 updateStatus('Error');
744744 }
745745 };
746-
746+
747747 const debouncedHandleLowVramChange = debounce(handleLowVramChange, 300);
748748 lowVramCheckbox.addEventListener('change', debouncedHandleLowVramChange);
749749 }
750-
750+
751751 // Other listeners without debounce since they don't need it
752752 const narratorVoiceSelect = document.getElementById('narrator_voice');
753753 if (narratorVoiceSelect) {
754754 narratorVoiceSelect.addEventListener('change', (event) => {
755755 this.settings.narrator_voice_gen = `${event.target.value}`;
756756 this.onSettingsChange();
757757 });
758758 }
759-
759+
760760 const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside');
761761 if (textNotInsideSelect) {
762762 textNotInsideSelect.addEventListener('change', (event) => {
763763 this.settings.text_not_inside = event.target.value;
764764 this.onSettingsChange();
765765 });
766766 }
767-
767+
768768 // AT Narrator Dropdown Listener
769769 const atNarratorSelect = document.getElementById('at_narrator_enabled');
770770 const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks');
771771 const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted');
772772 const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues');
773-
773+
774774 if (atNarratorSelect && textNotInsideSelect && narratorVoiceSelect) {
775775 atNarratorSelect.addEventListener('change', (event) => {
776776 const narratorOption = event.target.value;
777777 this.settings.narrator_enabled = narratorOption;
778-
778+
779779 const isNarratorDisabled = narratorOption === 'false';
780780 textNotInsideSelect.disabled = isNarratorDisabled;
781781 narratorVoiceSelect.disabled = isNarratorDisabled;
782-
782+
783783 if (narratorOption === 'true') {
784784 ttsPassAsterisksCheckbox.checked = false;
785785 $('#tts_pass_asterisks').click();
786786 $('#tts_pass_asterisks').trigger('change');
787787 ttsNarrateQuotedCheckbox.checked = true;
788788 ttsNarrateDialoguesCheckbox.checked = true;
789789 $('#tts_narrate_quoted').click();
790790 $('#tts_narrate_quoted').trigger('change');
791791 $('#tts_narrate_dialogues').click();
792792 $('#tts_narrate_dialogues').trigger('change');
793793 } else if (narratorOption === 'silent') {
794794 ttsPassAsterisksCheckbox.checked = false;
795795 $('#tts_pass_asterisks').click();
796796 $('#tts_pass_asterisks').trigger('change');
797797 } else {
798798 ttsPassAsterisksCheckbox.checked = true;
799799 $('#tts_pass_asterisks').click();
800800 $('#tts_pass_asterisks').trigger('change');
801801 }
802-
802+
803803 this.onSettingsChange();
804804 });
805805 }
806-
806+
807807 // Event Listener for AT Generation Method Dropdown
808808 const atGenerationMethodSelect = document.getElementById('at_generation_method');
809809 if (atGenerationMethodSelect) {
810810 atGenerationMethodSelect.addEventListener('change', (event) => {
811811 const selectedMethod = event.target.value;
812-
812+
813813 if (selectedMethod === 'streaming_enabled') {
814814 // Disable and unselect AT Narrator
815815 atNarratorSelect.disabled = true;
816816 atNarratorSelect.value = 'false';
817817 textNotInsideSelect.disabled = true;
818818 narratorVoiceSelect.disabled = true;
819819 } else if (selectedMethod === 'standard_generation') {
820820 // Enable AT Narrator
821821 atNarratorSelect.disabled = false;
822822 }
823823 this.settings.at_generation_method = selectedMethod;
824824 this.onSettingsChange();
825825 });
826826 }
827-
827+
828828 // Language Dropdown Listener
829829 const languageSelect = document.getElementById('language_options');
830830 if (languageSelect) {
831831 languageSelect.addEventListener('change', (event) => {
832832 this.settings.language = event.target.value;
833833 this.onSettingsChange();
834834 });
835835 }
836-
836+
837837 // AllTalk Endpoint Input Listener
838838 const atServerInput = document.getElementById('at_server');
839839 if (atServerInput) {
840840 atServerInput.addEventListener('input', (event) => {
841841 this.settings.provider_endpoint = event.target.value;
842842 this.onSettingsChange();
843843 });
844844 }
845845 }
846-
846+
847847 //#############################//
848848 // Store ST interface settings //
849849 //#############################//
850-
850+
851851 onSettingsChange() {
852852 // Update settings based on the UI elements
853853 //this.settings.provider_endpoint = $('#at_server').val();
854854 this.settings.language = $('#language_options').val();
855855 //this.settings.voiceMap = $('#voicemap').val();
856856 this.settings.at_generation_method = $('#at_generation_method').val();
857857 this.settings.narrator_enabled = $('#at_narrator_enabled').val();
858858 this.settings.at_narrator_text_not_inside = $('#at_narrator_text_not_inside').val();
859859 this.settings.rvc_character_voice = $('#rvc_character_voice').val();
860860 this.settings.rvc_narrator_voice = $('#rvc_narrator_voice').val();
861861 this.settings.rvc_character_pitch = $('#rvc_character_pitch').val();
862862 this.settings.rvc_narrator_pitch = $('#rvc_narrator_pitch').val();
863863 this.settings.narrator_voice_gen = $('#narrator_voice').val();
864864 // Save the updated settings
865865 saveTtsProviderSettings();
866866 }
867-
867+
868868 //#########################//
869869 // ST Handle Reload button //
870870 //#########################//
871-
871+
872872 async onRefreshClick() {
873873 try {
874874 updateStatus('Processing'); // Set status to Processing while refreshing
875875 await this.checkReady(); // Check if the TTS provider is ready
876876 await this.loadSettings(this.settings); // Reload the settings
877877 await this.checkReady(); // Check if the TTS provider is ready
878878 updateStatus(this.ready ? 'Ready' : 'Offline'); // Update the status based on readiness
879879 } catch (error) {
880880 console.error('Error during refresh:', error);
881881 updateStatus('Error'); // Set status to Error in case of failure
882882 }
883883 }
884-
884+
885885 //##################//
886886 // Preview AT Voice //
887887 //##################//
888-
888+
889889 async previewTtsVoice(voiceName) {
890890 try {
891891 // Prepare data for POST request
892892 const postData = new URLSearchParams();
893893 postData.append('voice', `${voiceName}`);
894-
894+
895895 // Add RVC parameters for V2 if applicable
896896 if (this.settings.server_version === 'v2' && this.settings.rvc_character_voice !== 'Disabled') {
897897 postData.append('rvccharacter_voice_gen', this.settings.rvc_character_voice);
898898 postData.append('rvccharacter_pitch', this.settings.rvc_character_pitch || '0');
899899 }
900-
900+
901901 // Making the POST request
902902 const response = await fetch(`${this.settings.provider_endpoint}/api/previewvoice/`, {
903903 method: 'POST',
904904 headers: {
905905 'Content-Type': 'application/x-www-form-urlencoded',
906906 },
907907 body: postData,
908908 });
909-
909+
910910 if (!response.ok) {
911911 const errorText = await response.text();
912912 console.error('previewTtsVoice Error Response Text:', errorText);
913913 throw new Error(`HTTP ${response.status}: ${errorText}`);
914914 }
915-
915+
916916 const data = await response.json();
917917 if (data.output_file_url) {
918918 // Handle V1/V2 URL differences
919919 const fullUrl = this.settings.server_version === 'v1'
920920 ? data.output_file_url
921921 : `${this.settings.provider_endpoint}${data.output_file_url}`;
922-
922+
923923 const audioElement = new Audio(fullUrl);
924924 audioElement.play().catch(e => console.error('Error playing audio:', e));
925925 } else {
926926 console.warn('previewTtsVoice No output file URL received in the response');
927927 throw new Error('No output file URL received in the response');
928928 }
929929 } catch (error) {
930930 console.error('previewTtsVoice Exception caught during preview generation:', error);
931931 throw error;
932932 }
933933 }
934-
934+
935935 //#####################//
936936 // Populate ST voices //
937937 //#####################//
938-
938+
939939 async getVoice(voiceName, generatePreview = false) {
940940 // Ensure this.voices is populated
941941 if (this.voices.length === 0) {
942942 // Fetch voice objects logic
943943 }
944944 // Find the object where the name matches voiceName
945945 const match = this.voices.find(voice => voice.name === voiceName);
946946 if (!match) {
947947 // Error handling
948948 }
949949 // Generate preview URL only if requested
950950 if (!match.preview_url && generatePreview) {
951951 // Generate preview logic
952952 }
953953 return match; // Return the found voice object
954954 }
955-
955+
956956 //##########################################//
957957 // Generate TTS Streaming or call Standard //
958958 //##########################################//
959-
959+
960960 async generateTts(inputText, voiceId) {
961961 try {
962962 if (this.settings.at_generation_method === 'streaming_enabled') {
963963 // Construct the streaming URL
964964 const streamingUrl = `${this.settings.provider_endpoint}/api/tts-generate-streaming?text=${encodeURIComponent(inputText)}&voice=${encodeURIComponent(voiceId)}&language=${encodeURIComponent(this.settings.language)}&output_file=stream_output.wav`;
965965 console.log('Streaming URL:', streamingUrl);
966-
966+
967967 // Return the streaming URL directly
968968 return streamingUrl;
969969 } else {
970970 // For standard method
971971 const outputUrl = await this.fetchTtsGeneration(inputText, voiceId);
972972 const audioResponse = await fetch(outputUrl);
973973 if (!audioResponse.ok) {
974974 throw new Error(`HTTP ${audioResponse.status}: Failed to fetch audio data`);
975975 }
976976 return audioResponse; // Return the fetch response directly
977977 }
978978 } catch (error) {
979979 console.error('Error in generateTts:', error);
980980 throw error;
981981 }
982982 }
983-
983+
984-
984+
985985 //####################//
986986 // Generate Standard //
987987 //####################//
988-
988+
989989 async fetchTtsGeneration(inputText, voiceId) {
990990 const requestBody = new URLSearchParams({
991991 'text_input': inputText,
992992 'text_filtering': 'standard',
993993 'character_voice_gen': voiceId,
994994 'narrator_enabled': this.settings.narrator_enabled,
995995 'narrator_voice_gen': this.settings.narrator_voice_gen,
996996 'text_not_inside': this.settings.at_narrator_text_not_inside,
997997 'language': this.settings.language,
998998 'output_file_name': 'st_output',
999999 'output_file_timestamp': 'true',
10001000 'autoplay': 'false',
10011001 'autoplay_volume': '0.8',
10021002 });
1003-
1003+
10041004 // Add RVC parameters only for V2
10051005 if (this.settings.server_version === 'v2') {
10061006 if (this.settings.rvc_character_voice !== 'Disabled') {
10071007 requestBody.append('rvccharacter_voice_gen', this.settings.rvc_character_voice);
10081008 requestBody.append('rvccharacter_pitch', this.settings.rvc_character_pitch || '0');
10091009 }
10101010 if (this.settings.rvc_narrator_voice !== 'Disabled') {
10111011 requestBody.append('rvcnarrator_voice_gen', this.settings.rvc_narrator_voice);
10121012 requestBody.append('rvcnarrator_pitch', this.settings.rvc_narrator_pitch || '0');
10131013 }
10141014 }
1015-
1015+
10161016 try {
10171017 const response = await doExtrasFetch(
10181018 `${this.settings.provider_endpoint}/api/tts-generate`,
10191019 {
10201020 method: 'POST',
10211021 headers: {
10221022 'Content-Type': 'application/x-www-form-urlencoded',
10231023 'Cache-Control': 'no-cache',
10241024 },
10251025 body: requestBody,
10261026 },
10271027 );
1028-
1028+
10291029 if (!response.ok) {
10301030 const errorText = await response.text();
10311031 console.error('fetchTtsGeneration Error Response Text:', errorText);
10321032 throw new Error(`HTTP ${response.status}: ${errorText}`);
10331033 }
10341034 const data = await response.json();
1035-
1035+
10361036 // Handle V1/V2 URL differences
10371037 const outputUrl = this.settings.server_version === 'v1'
10381038 ? data.output_file_url // V1 returns full URL
10391039 : `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path
1040-
1040+
10411041 return outputUrl;
10421042 } catch (error) {
10431043 console.error('[fetchTtsGeneration] Exception caught:', error);
10441044 throw error;
10451045 }
10461046 }
10471047}
1048-
1048+
10491049//#########################//
10501050// Update Status Messages //
10511051//#########################//
1052-
1052+
10531053function updateStatus(message) {
10541054 const statusElement = document.getElementById('status_info');
10551055 if (statusElement) {
10561056 statusElement.textContent = message;
10571057 switch (message) {
10581058 case 'Offline':
10591059 statusElement.style.color = 'red';
10601060 break;
10611061 case 'Ready':
10621062 statusElement.style.color = 'lightgreen';
10631063 break;
10641064 case 'Processing':
10651065 statusElement.style.color = 'blue';
10661066 break;
10671067 case 'Error':
10681068 statusElement.style.color = 'red';
10691069 break;
10701070 }
10711071 }
10721072}
public/scripts/filters.js+0 -1
@@ -448,6 +448,5 @@ export class FilterHelper {
448448 for (const cache of Object.values(this.fuzzySearchCaches)) {
449449 cache.resultMap.clear();
450450 }
451- console.log('All fuzzy search caches cleared');
452451 }
453452}
public/scripts/group-chats.js+3 -2
@@ -1265,7 +1265,7 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) {
12651265 const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId);
12661266 let candidates = characters
12671267 .filter((x) => isGroupMember(thisGroup, x.avatar) == onlyMembers)
12681268 .map((x, index) => ({ item: x, id: indexcharacters.indexOf(x), type: 'character' }));
12691269
12701270 if (doFilter) {
12711271 candidates = groupCandidatesFilter.applyFilters(candidates);
@@ -1275,9 +1275,10 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) {
12751275 candidates.sort(sortMembersFn);
12761276 } else {
12771277 const useFilterOrder = doFilter && !!$('#rm_group_filter').val();
12781278 sortEntitiesList(candidates, useFilterOrder, groupCandidatesFilter);
12791279 }
12801280
1281+ groupCandidatesFilter.clearFuzzySearchCaches();
12811282 return candidates;
12821283}
12831284
public/scripts/openai.js+9 -5
@@ -1182,7 +1182,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
11821182
11831183 // Apply character-specific main prompt
11841184 const systemPrompt = prompts.get('main') ?? null;
1185- if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true) {
1185+ const isSystemPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('main');
1186+ if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true && !isSystemPromptDisabled) {
11861187 const mainOriginalContent = systemPrompt.content;
11871188 systemPrompt.content = systemPromptOverride;
11881189 const mainReplacement = promptManager.preparePrompt(systemPrompt, mainOriginalContent);
@@ -1191,7 +1192,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name
11911192
11921193 // Apply character-specific jailbreak
11931194 const jailbreakPrompt = prompts.get('jailbreak') ?? null;
1194- if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true) {
1195+ const isJailbreakPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('jailbreak');
1196+ if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true && !isJailbreakPromptDisabled) {
11951197 const jbOriginalContent = jailbreakPrompt.content;
11961198 jailbreakPrompt.content = jailbreakPromptOverride;
11971199 const jbReplacement = promptManager.preparePrompt(jailbreakPrompt, jbOriginalContent);
@@ -4081,9 +4083,9 @@ async function onModelChange() {
40814083 $('#openai_max_context').attr('max', max_2mil);
40824084 } else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121')) {
40834085 $('#openai_max_context').attr('max', max_32k);
40844086 } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
40854087 $('#openai_max_context').attr('max', max_2mil);
40864088 } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) {
40874089 $('#openai_max_context').attr('max', max_1mil);
40884090 } else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') {
40894091 $('#openai_max_context').attr('max', max_16k);
@@ -4273,7 +4275,7 @@ async function onModelChange() {
42734275 else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
42744276 $('#openai_max_context').attr('max', max_8k);
42754277 }
42764278 else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
42774279 $('#openai_max_context').attr('max', max_128k);
42784280 }
42794281 else if (oai_settings.groq_model.includes('llama3-groq')) {
@@ -4755,6 +4757,7 @@ export function isImageInliningSupported() {
47554757 // gultra just isn't being offered as multimodal, thanks google.
47564758 const visionSupportedModels = [
47574759 'gpt-4-vision',
4760+ 'gemini-2.0-flash-exp',
47584761 'gemini-1.5-flash',
47594762 'gemini-1.5-flash-latest',
47604763 'gemini-1.5-flash-001',
@@ -4765,6 +4768,7 @@ export function isImageInliningSupported() {
47654768 'gemini-1.5-flash-8b-exp-0924',
47664769 'gemini-exp-1114',
47674770 'gemini-exp-1121',
4771+ 'gemini-exp-1206',
47684772 'gemini-1.0-pro-vision-latest',
47694773 'gemini-1.5-pro',
47704774 'gemini-1.5-pro-latest',
public/scripts/personas.js+68 -2
@@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
2121import { debounce_timeout } from './constants.js';
2222import { FILTER_TYPES, FilterHelper } from './filters.js';
2323import { selected_group } from './group-chats.js';
2424import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
2525import { t } from './i18n.js';
26+import { openWorldInfoEditor, world_names } from './world-info.js';
27+import { renderTemplateAsync } from './templates.js';
2628
2729let savePersonasPage = 0;
2830const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
375377 position: persona_description_positions.IN_PROMPT,
376378 depth: DEFAULT_DEPTH,
377379 role: DEFAULT_ROLE,
380+ lorebook: '',
378381 };
379382
380383 saveSettingsDebounced();
@@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) {
418421 position: persona_description_positions.IN_PROMPT,
419422 depth: DEFAULT_DEPTH,
420423 role: DEFAULT_ROLE,
424+ lorebook: '',
421425 };
422426
423427 // If the user is currently using this persona, update the description
@@ -461,6 +465,7 @@ export function setPersonaDescription() {
461465 .val(power_user.persona_description_role)
462466 .find(`option[value="${power_user.persona_description_role}"]`)
463467 .prop('selected', String(true));
468+ $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
464469 countPersonaDescriptionTokens();
465470}
466471
@@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) {
490495 position: persona_description_positions.IN_PROMPT,
491496 depth: DEFAULT_DEPTH,
492497 role: DEFAULT_ROLE,
498+ lorebook: '',
493499 };
494500 console.log(`Created persona name for ${avatarId} as ${newName}`);
495501 }
@@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) {
535541 position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
536542 depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH,
537543 role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE,
544+ lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '',
538545 };
539546 }
540547
@@ -579,12 +586,20 @@ function selectCurrentPersona() {
579586 power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
580587 power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
581588 power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
589+ power_user.persona_description_lorebook = descriptor.lorebook ?? '';
582590 } else {
583591 power_user.persona_description = '';
584592 power_user.persona_description_position = persona_description_positions.IN_PROMPT;
585593 power_user.persona_description_depth = DEFAULT_DEPTH;
586594 power_user.persona_description_role = DEFAULT_ROLE;
587- power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE };
595+ power_user.persona_description_lorebook = '';
596+ power_user.persona_descriptions[user_avatar] = {
597+ description: '',
598+ position: persona_description_positions.IN_PROMPT,
599+ depth: DEFAULT_DEPTH,
600+ role: DEFAULT_ROLE,
601+ lorebook: '',
602+ };
588603 }
589604
590605 setPersonaDescription();
@@ -652,6 +667,7 @@ async function lockPersona() {
652667 position: persona_description_positions.IN_PROMPT,
653668 depth: DEFAULT_DEPTH,
654669 role: DEFAULT_ROLE,
670+ lorebook: '',
655671 };
656672 }
657673
@@ -731,6 +747,7 @@ function onPersonaDescriptionInput() {
731747 position: Number($('#persona_description_position').find(':selected').val()),
732748 depth: Number($('#persona_depth_value').val()),
733749 role: Number($('#persona_depth_role').find(':selected').val()),
750+ lorebook: '',
734751 };
735752 power_user.persona_descriptions[user_avatar] = object;
736753 }
@@ -766,6 +783,52 @@ function onPersonaDescriptionDepthRoleInput() {
766783 saveSettingsDebounced();
767784}
768785
786+/**
787+ * Opens a popup to set the lorebook for the current persona.
788+ * @param {PointerEvent} event Click event
789+ */
790+async function onPersonaLoreButtonClick(event) {
791+ const personaName = power_user.personas[user_avatar];
792+ const selectedLorebook = power_user.persona_description_lorebook;
793+
794+ if (!personaName) {
795+ toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`);
796+ return;
797+ }
798+
799+ if (event.altKey && selectedLorebook) {
800+ openWorldInfoEditor(selectedLorebook);
801+ return;
802+ }
803+
804+ const template = $(await renderTemplateAsync('personaLorebook'));
805+
806+ const worldSelect = template.find('select');
807+ template.find('.persona_name').text(personaName);
808+
809+ for (const worldName of world_names) {
810+ const option = document.createElement('option');
811+ option.value = worldName;
812+ option.innerText = worldName;
813+ option.selected = selectedLorebook === worldName;
814+ worldSelect.append(option);
815+ }
816+
817+ worldSelect.on('change', function () {
818+ power_user.persona_description_lorebook = String($(this).val());
819+
820+ if (power_user.personas[user_avatar]) {
821+ const object = getOrCreatePersonaDescriptor();
822+ object.lorebook = power_user.persona_description_lorebook;
823+ }
824+
825+ $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
826+ saveSettingsDebounced();
827+ });
828+
829+ await callGenericPopup(template, POPUP_TYPE.TEXT);
830+}
831+
769832function onPersonaDescriptionPositionInput() {
770833 power_user.persona_description_position = Number(
771834 $('#persona_description_position').find(':selected').val(),
@@ -789,6 +852,7 @@ function getOrCreatePersonaDescriptor() {
789852 position: power_user.persona_description_position,
790853 depth: power_user.persona_description_depth,
791854 role: power_user.persona_description_role,
855+ lorebook: power_user.persona_description_lorebook,
792856 };
793857 power_user.persona_descriptions[user_avatar] = object;
794858 }
@@ -1038,6 +1102,7 @@ async function duplicatePersona(avatarId) {
10381102 position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
10391103 depth: descriptor?.depth ?? DEFAULT_DEPTH,
10401104 role: descriptor?.role ?? DEFAULT_ROLE,
1105+ lorebook: descriptor?.lorebook ?? '',
10411106 };
10421107
10431108 await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
@@ -1055,6 +1120,7 @@ export function initPersonas() {
10551120 $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
10561121 $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
10571122 $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
1123+ $('#persona_lore_button').on('click', onPersonaLoreButtonClick);
10581124 $('#personas_backup').on('click', onBackupPersonas);
10591125 $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
10601126 $('#personas_restore_input').on('change', onPersonasRestoreInput);
public/scripts/power-user.js+15 -13
@@ -261,6 +261,7 @@ let power_user = {
261261 persona_description_position: persona_description_positions.IN_PROMPT,
262262 persona_description_role: 0,
263263 persona_description_depth: 2,
264+ persona_description_lorebook: '',
264265 persona_show_notifications: true,
265266 persona_sort_order: 'asc',
266267
@@ -472,9 +473,9 @@ function switchCompactInputArea() {
472473 $('#compact_input_area').prop('checked', power_user.compact_input_area);
473474}
474475
475476export function switchSwipeNumAllMessages() {
476477 $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
477478 $('.mes:not(.last_mes) .swipes-counterbody').csstoggleClass('opacityswipeAllMessages', '').toggle(!!power_user.show_swipe_num_all_messages);
478479}
479480
480481var originalSliderValues = [];
@@ -1756,7 +1757,7 @@ async function loadContextSettings() {
17561757 } else {
17571758 $element.val(power_user.context[control.property]);
17581759 }
17591760 console.logdebug(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
17601761
17611762 // If the setting already exists, no need to duplicate it
17621763 // TODO: Maybe check the power_user object for the setting instead of a flag?
@@ -1767,7 +1768,7 @@ async function loadContextSettings() {
17671768 } else {
17681769 power_user.context[control.property] = value;
17691770 }
17701771 console.logdebug(`Setting ${$element.prop('id')} to ${value}`);
17711772 if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) {
17721773 await resetScrollHeight($(this));
17731774 }
@@ -1836,7 +1837,7 @@ async function loadContextSettings() {
18361837 * Common function to perform fuzzy search with optional caching
18371838 * @param {string} type - Type of search from fuzzySearchCategories
18381839 * @param {any[]} data - Data array to search in
18391840 * @param {Array<{name: string, weight: number, getFn?: Function(obj: any) => string}>} keys - Fuse.js keys configuration
18401841 * @param {string} searchValue - The search term
18411842 * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
18421843 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
@@ -1850,7 +1851,6 @@ function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = n
18501851 }
18511852 }
18521853
1853- // @ts-ignore
18541854 const fuse = new Fuse(data, {
18551855 keys: keys,
18561856 includeScore: true,
@@ -1924,7 +1924,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null)
19241924 const mappedData = data.map(x => ({
19251925 key: x,
19261926 name: power_user.personas[x] ?? '',
19271927 description: power_user.persona_descriptions[x]?.description ?? '',
19281928 }));
19291929
19301930 const keys = [
@@ -2080,19 +2080,21 @@ const compareFunc = (first, second) => {
20802080 * Sorts an array of entities based on the current sort settings
20812081 * @param {any[]} entities An array of objects with an `item` property
20822082 * @param {boolean} forceSearch Whether to force search sorting
2083+ * @param {import('./filters.js').FilterHelper} [filterHelper=null] Filter helper to use
20832084 */
20842085export function sortEntitiesList(entities, forceSearch, filterHelper = null) {
2086+ filterHelper = filterHelper ?? entitiesFilter;
20852087 if (power_user.sort_field == undefined || entities.length === 0) {
20862088 return;
20872089 }
20882090
2089- if (power_user.sort_order === 'random') {
2091+ const isSearch = forceSearch || $('#character_sort_order option[data-field="search"]').is(':selected');
2092+
2093+ if (!isSearch && power_user.sort_order === 'random') {
20902094 shuffle(entities);
20912095 return;
20922096 }
20932097
2094- const isSearch = forceSearch || $('#character_sort_order option[data-field="search"]').is(':selected');
2095-
20962098 entities.sort((a, b) => {
20972099 // Sort tags/folders will always be at the top
20982100 if (a.type === 'tag' && b.type !== 'tag') {
@@ -2104,8 +2106,8 @@ export function sortEntitiesList(entities, forceSearch) {
21042106
21052107 // If we have search sorting, we take scores and use those
21062108 if (isSearch) {
21072109 const aScore = entitiesFilterfilterHelper.getScore(FILTER_TYPES.SEARCH, `${a.type}.${a.id}`);
21082110 const bScore = entitiesFilterfilterHelper.getScore(FILTER_TYPES.SEARCH, `${b.type}.${b.id}`);
21092111 return (aScore - bScore);
21102112 }
21112113
public/scripts/samplerSelect.js+13 -0
@@ -129,6 +129,10 @@ function setSamplerListListeners() {
129129 relatedDOMElement = $('#sampler_priority_block_ooba');
130130 }
131131
132+ if (samplerName === 'samplers_priorities') { //this is for aphrodite's sampler priority
133+ relatedDOMElement = $('#sampler_priority_block_aphrodite');
134+ }
135+
132136 if (samplerName === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
133137 relatedDOMElement = $('#contrastiveSearchBlock');
134138 }
@@ -237,6 +241,11 @@ async function listSamplers(main_api, arrayOnly = false) {
237241 displayname = 'Ooba Sampler Priority Block';
238242 }
239243
244+ if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority
245+ targetDOMelement = $('#sampler_priority_block_aphrodite');
246+ displayname = 'Aphrodite Sampler Priority Block';
247+ }
248+
240249 if (sampler === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
241250 targetDOMelement = $('#contrastiveSearchBlock');
242251 displayname = 'Contrast Search Block';
@@ -373,6 +382,10 @@ export async function validateDisabledSamplers(redraw = false) {
373382 relatedDOMElement = $('#sampler_priority_block_ooba');
374383 }
375384
385+ if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority
386+ relatedDOMElement = $('#sampler_priority_block_aphrodite');
387+ }
388+
376389 if (sampler === 'dry_multiplier') {
377390 relatedDOMElement = $('#dryBlock');
378391 targetDisplayType = 'block';
public/scripts/st-context.js+171 -0
@@ -0,0 +1,171 @@
1+import {
2+ activateSendButtons,
3+ addOneMessage,
4+ callPopup,
5+ characters,
6+ chat,
7+ chat_metadata,
8+ create_save,
9+ deactivateSendButtons,
10+ event_types,
11+ eventSource,
12+ extension_prompts,
13+ Generate,
14+ generateQuietPrompt,
15+ getCurrentChatId,
16+ getRequestHeaders,
17+ getThumbnailUrl,
18+ main_api,
19+ max_context,
20+ menu_type,
21+ messageFormatting,
22+ name1,
23+ name2,
24+ online_status,
25+ openCharacterChat,
26+ reloadCurrentChat,
27+ renameChat,
28+ saveChatConditional,
29+ saveMetadata,
30+ saveReply,
31+ saveSettingsDebounced,
32+ selectCharacterById,
33+ sendGenerationRequest,
34+ sendStreamingRequest,
35+ sendSystemMessage,
36+ setExtensionPrompt,
37+ stopGeneration,
38+ streamingProcessor,
39+ substituteParams,
40+ substituteParamsExtended,
41+ this_chid,
42+ updateChatMetadata,
43+} from '../script.js';
44+import {
45+ extension_settings,
46+ ModuleWorkerWrapper,
47+ renderExtensionTemplate,
48+ renderExtensionTemplateAsync,
49+ writeExtensionField,
50+} from './extensions.js';
51+import { groups, openGroupChat, selected_group } from './group-chats.js';
52+import { t, translate } from './i18n.js';
53+import { hideLoader, showLoader } from './loader.js';
54+import { MacrosParser } from './macros.js';
55+import { oai_settings } from './openai.js';
56+import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
57+import { power_user, registerDebugFunction } from './power-user.js';
58+import { isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
59+import { ScraperManager } from './scrapers.js';
60+import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
61+import { SlashCommand } from './slash-commands/SlashCommand.js';
62+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
63+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
64+import { tag_map, tags } from './tags.js';
65+import { textgenerationwebui_settings } from './textgen-settings.js';
66+import { getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
67+import { ToolManager } from './tool-calling.js';
68+import { timestampToMoment } from './utils.js';
69+
70+export function getContext() {
71+ return {
72+ chat,
73+ characters,
74+ groups,
75+ name1,
76+ name2,
77+ characterId: this_chid,
78+ groupId: selected_group,
79+ chatId: selected_group
80+ ? groups.find(x => x.id == selected_group)?.chat_id
81+ : (characters[this_chid]?.chat),
82+ getCurrentChatId,
83+ getRequestHeaders,
84+ reloadCurrentChat,
85+ renameChat,
86+ saveSettingsDebounced,
87+ onlineStatus: online_status,
88+ maxContext: Number(max_context),
89+ chatMetadata: chat_metadata,
90+ streamingProcessor,
91+ eventSource,
92+ eventTypes: event_types,
93+ addOneMessage,
94+ generate: Generate,
95+ sendStreamingRequest,
96+ sendGenerationRequest,
97+ stopGeneration,
98+ /** @deprecated Use getTokenCountAsync instead */
99+ getTokenCount,
100+ getTokenCountAsync,
101+ extensionPrompts: extension_prompts,
102+ setExtensionPrompt,
103+ updateChatMetadata,
104+ saveChat: saveChatConditional,
105+ openCharacterChat,
106+ openGroupChat,
107+ saveMetadata,
108+ sendSystemMessage,
109+ activateSendButtons,
110+ deactivateSendButtons,
111+ saveReply,
112+ substituteParams,
113+ substituteParamsExtended,
114+ SlashCommandParser,
115+ SlashCommand,
116+ SlashCommandArgument,
117+ SlashCommandNamedArgument,
118+ ARGUMENT_TYPE,
119+ executeSlashCommandsWithOptions,
120+ /** @deprecated Use SlashCommandParser.addCommandObject() instead */
121+ registerSlashCommand,
122+ /** @deprecated Use executeSlashCommandWithOptions instead */
123+ executeSlashCommands,
124+ timestampToMoment,
125+ /** @deprecated Handlebars for extensions are no longer supported. */
126+ registerHelper: () => { },
127+ registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
128+ unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
129+ registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
130+ unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
131+ isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
132+ canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
133+ registerDebugFunction,
134+ /** @deprecated Use renderExtensionTemplateAsync instead. */
135+ renderExtensionTemplate,
136+ renderExtensionTemplateAsync,
137+ registerDataBankScraper: ScraperManager.registerDataBankScraper.bind(ScraperManager),
138+ /** @deprecated Use callGenericPopup or Popup instead. */
139+ callPopup,
140+ callGenericPopup,
141+ showLoader,
142+ hideLoader,
143+ mainApi: main_api,
144+ extensionSettings: extension_settings,
145+ ModuleWorkerWrapper,
146+ getTokenizerModel,
147+ generateQuietPrompt,
148+ writeExtensionField,
149+ getThumbnailUrl,
150+ selectCharacterById,
151+ messageFormatting,
152+ shouldSendOnEnter,
153+ isMobile,
154+ t,
155+ translate,
156+ tags,
157+ tagMap: tag_map,
158+ menuType: menu_type,
159+ createCharacterData: create_save,
160+ /** @deprecated Legacy snake-case naming, compatibility with old extensions */
161+ event_types: event_types,
162+ Popup,
163+ POPUP_TYPE,
164+ POPUP_RESULT,
165+ chatCompletionSettings: oai_settings,
166+ textCompletionSettings: textgenerationwebui_settings,
167+ powerUserSettings: power_user,
168+ };
169+}
170+
171+export default getContext;
public/scripts/templates/chatLorebook.html+18 -0
@@ -0,0 +1,18 @@
1+<div class="chat_world range-block flexFlowColumn flex-container">
2+ <div class="range-block-title">
3+ <h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
4+ <span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
5+ </h4>
6+ </div>
7+ <div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
8+ <span data-i18n="chat_world_template_txt">
9+ A selected World Info will be bound to this chat. When generating an AI reply,
10+ it will be combined with the entries from global and character lorebooks.
11+ </span>
12+ </div>
13+ <div class="range-block-range wide100p">
14+ <select class="chat_world_info_selector wide100p">
15+ <option value="">--- None ---</option>
16+ </select>
17+ </div>
18+</div>
public/scripts/templates/personaLorebook.html+18 -0
@@ -0,0 +1,18 @@
1+<div class="persona_world range-block flexFlowColumn flex-container">
2+ <div class="range-block-title">
3+ <h4>
4+ <span data-i18n="PErsona Lorebook for">Persona Lorebook for</span> <span class="persona_name"></span>
5+ </h4>
6+ </div>
7+ <div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
8+ <span data-i18n="persona_world_template_txt">
9+ A selected World Info will be bound to this persona. When generating an AI reply,
10+ it will be combined with the entries from global, character and chat lorebooks.
11+ </span>
12+ </div>
13+ <div class="range-block-range wide100p">
14+ <select class="persona_world_info_selector wide100p">
15+ <option value="">--- None ---</option>
16+ </select>
17+ </div>
18+</div>
public/scripts/templates/tagManagement.html+5 -5
@@ -20,14 +20,14 @@
2020 <small>
2121 <span data-i18n="Drag handle to reorder. Click name to rename. Click color to change display.">Drag handle to reorder. Click name to rename. Click color to change display.</span><br>
2222 {{#if bogus_folders}}<span data-i18n="Click on the folder icon to use this tag as a folder.">Click on the folder icon to use this tag as a folder.</span><br>{{/if}}
2323 <label class="checkboxcheckbox_label flex-container alignitemscenter flexNoGapalignItemsCenter m-t-1" for="auto_sort_tags">
2424 <input type="checkbox" id="auto_sort_tags" name="auto_sort_tags" {{#if auto_sort_tags}} checked{{/if}} />
2525 <span data-i18n="Use alphabetical sorting">
2626 Use alphabetical sorting
2727 </span>
2828 <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]tags_sorting_desc"
2929 title="If enabled, tags will automatically be sorted alphabetically on creation or rename.\nIf&#10;If disabled, new tags will be appended at the end.\n\nIf&#10;&#10;If a tag is manually reordered by dragging, automatic sorting will be disabled.">
3030 </div>
3131 </label>
3232 </small>
33- </div>
33 \ No newline at end of file
33+ </div>
public/scripts/textgen-settings.js+73 -11
@@ -16,14 +16,7 @@ import { power_user, registerDebugFunction } from './power-user.js';
1616import { getEventSourceStream } from './sse-stream.js';
1717import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer } from './textgen-models.js';
1818import { ENCODE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
1919import { getSortableDelay, onlyUnique, arraysEqual } from './utils.js';
20-
21-export {
22- settings as textgenerationwebui_settings,
23- loadTextGenSettings,
24- generateTextGenWithStreaming,
25- formatTextGenURL,
26-};
2720
2821export const textgen_types = {
2922 OOBA: 'ooba',
@@ -89,6 +82,22 @@ const OOBA_DEFAULT_ORDER = [
8982 'encoder_repetition_penalty',
9083 'no_repeat_ngram',
9184];
85+const APHRODITE_DEFAULT_ORDER = [
86+ 'dry',
87+ 'penalties',
88+ 'no_repeat_ngram',
89+ 'temperature',
90+ 'top_nsigma',
91+ 'top_p_top_k',
92+ 'top_a',
93+ 'min_p',
94+ 'tfs',
95+ 'eta_cutoff',
96+ 'epsilon_cutoff',
97+ 'typical_p',
98+ 'quadratic',
99+ 'xtc',
100+];
92101const BIAS_KEY = '#textgenerationwebui_api-settings';
93102
94103// Maybe let it be configurable in the future?
@@ -170,6 +179,7 @@ const settings = {
170179 banned_tokens: '',
171180 sampler_priority: OOBA_DEFAULT_ORDER,
172181 samplers: LLAMACPP_DEFAULT_ORDER,
182+ samplers_priorities: APHRODITE_DEFAULT_ORDER,
173183 ignore_eos_token: false,
174184 spaces_between_special_tokens: true,
175185 speculative_ngram: false,
@@ -197,6 +207,10 @@ const settings = {
197207 featherless_model: '',
198208};
199209
210+export {
211+ settings as textgenerationwebui_settings,
212+};
213+
200214export let textgenerationwebui_banned_in_macros = [];
201215
202216export let textgenerationwebui_presets = [];
@@ -259,6 +273,7 @@ export const setting_names = [
259273 'sampler_order',
260274 'sampler_priority',
261275 'samplers',
276+ 'samplers_priorities',
262277 'n',
263278 'logit_bias',
264279 'custom_model',
@@ -327,7 +342,7 @@ async function selectPreset(name) {
327342 saveSettingsDebounced();
328343}
329344
330345export function formatTextGenURL(value) {
331346 try {
332347 const noFormatTypes = [MANCER, TOGETHERAI, INFERMATICAI, DREAMGEN, OPENROUTER];
333348 if (noFormatTypes.includes(settings.type)) {
@@ -465,7 +480,7 @@ function calculateLogitBias() {
465480 return result;
466481}
467482
468483export function loadTextGenSettings(data, loadedSettings) {
469484 textgenerationwebui_presets = convertPresets(data.textgenerationwebui_presets);
470485 textgenerationwebui_preset_names = data.textgenerationwebui_preset_names ?? [];
471486 Object.assign(settings, loadedSettings.textgenerationwebui_settings ?? {});
@@ -556,6 +571,20 @@ function sortOobaItemsByOrder(orderArray) {
556571 });
557572}
558573
574+/**
575+ * Sorts the Aphrodite sampler items by the given order.
576+ * @param {string[]} orderArray Sampler order array.
577+ */
578+function sortAphroditeItemsByOrder(orderArray) {
579+ console.debug('Preset samplers order: ', orderArray);
580+ const $container = $('#sampler_priority_container_aphrodite');
581+
582+ orderArray.forEach((name) => {
583+ const $item = $container.find(`[data-name="${name}"]`).detach();
584+ $container.append($item);
585+ });
586+}
587+
559588jQuery(function () {
560589 $('#koboldcpp_order').sortable({
561590 delay: getSortableDelay(),
@@ -609,6 +638,19 @@ jQuery(function () {
609638 },
610639 });
611640
641+ $('#sampler_priority_container_aphrodite').sortable({
642+ delay: getSortableDelay(),
643+ stop: function () {
644+ const order = [];
645+ $('#sampler_priority_container_aphrodite').children().each(function () {
646+ order.push($(this).data('name'));
647+ });
648+ settings.samplers_priorities = order;
649+ console.log('Samplers reordered:', settings.samplers_priorities);
650+ saveSettingsDebounced();
651+ },
652+ });
653+
612654 $('#tabby_json_schema').on('input', function () {
613655 const json_schema_string = String($(this).val());
614656
@@ -627,6 +669,13 @@ jQuery(function () {
627669 saveSettingsDebounced();
628670 });
629671
672+ $('#aphrodite_default_order').on('click', function () {
673+ sortAphroditeItemsByOrder(APHRODITE_DEFAULT_ORDER);
674+ settings.samplers_priorities = APHRODITE_DEFAULT_ORDER;
675+ console.log('Default samplers order loaded:', settings.samplers_priorities);
676+ saveSettingsDebounced();
677+ });
678+
630679 $('#textgen_type').on('change', function () {
631680 const type = String($(this).val());
632681 settings.type = type;
@@ -835,6 +884,14 @@ function setSettingByName(setting, value, trigger) {
835884 return;
836885 }
837886
887+ if ('samplers_priorities' === setting) {
888+ value = Array.isArray(value) ? value : APHRODITE_DEFAULT_ORDER;
889+ insertMissingArrayItems(APHRODITE_DEFAULT_ORDER, value);
890+ sortAphroditeItemsByOrder(value);
891+ settings.samplers_priorities = value;
892+ return;
893+ }
894+
838895 if ('samplers' === setting) {
839896 value = Array.isArray(value) ? value : LLAMACPP_DEFAULT_ORDER;
840897 insertMissingArrayItems(LLAMACPP_DEFAULT_ORDER, value);
@@ -889,7 +946,7 @@ function setSettingByName(setting, value, trigger) {
889946 * @returns {Promise<(function(): AsyncGenerator<{swipes: [], text: string, toolCalls: [], logprobs: {token: string, topLogprobs: Candidate[]}|null}, void, *>)|*>}
890947 * @throws {Error} - If the response status is not OK, or from within the generator
891948 */
892949export async function generateTextGenWithStreaming(generate_data, signal) {
893950 generate_data.stream = true;
894951
895952 const response = await fetch('/api/backends/text-completions/generate', {
@@ -1259,6 +1316,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
12591316 'nsigma': settings.nsigma,
12601317 'custom_token_bans': toIntArray(banned_tokens),
12611318 'no_repeat_ngram_size': settings.no_repeat_ngram_size,
1319+ 'sampler_priority': settings.type === APHRODITE && !arraysEqual(
1320+ settings.samplers_priorities,
1321+ APHRODITE_DEFAULT_ORDER)
1322+ ? settings.samplers_priorities
1323+ : undefined,
12621324 };
12631325
12641326 if (settings.type === OPENROUTER) {
public/scripts/tokenizers.js+20 -10
@@ -8,8 +8,6 @@ import { kai_flags } from './kai-settings.js';
88import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer, getTextGenModel } from './textgen-settings.js';
99import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js';
1010
11-const { OOBA, TABBY, KOBOLDCPP, VLLM, APHRODITE, LLAMACPP, OPENROUTER, DREAMGEN } = textgen_types;
12-
1311export const CHARACTERS_PER_TOKEN_RATIO = 3.35;
1412export const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
1513export const TOKENIZER_SUPPORTED_KEY = 'tokenizationSupported';
@@ -52,8 +50,12 @@ export const ENCODE_TOKENIZERS = [
5250 //tokenizers.NERD2,
5351];
5452
55-// A list of Text Completion sources that support remote tokenization.
53+/**
56-export const TEXTGEN_TOKENIZERS = [OOBA, TABBY, KOBOLDCPP, LLAMACPP, VLLM, APHRODITE];
54+ * A list of Text Completion sources that support remote tokenization.
55+ * Populated in initTokenziers due to circular dependencies.
56+ * @type {string[]}
57+ */
58+export const TEXTGEN_TOKENIZERS = [];
5759
5860const TOKENIZER_URLS = {
5961 [tokenizers.GPT2]: {
@@ -287,7 +289,7 @@ export function getTokenizerBestMatch(forApi) {
287289 const hasTokenizerError = sessionStorage.getItem(TOKENIZER_WARNING_KEY);
288290 const hasValidEndpoint = sessionStorage.getItem(TOKENIZER_SUPPORTED_KEY);
289291 const isConnected = online_status !== 'no_connection';
290292 const isTokenizerSupported = TEXTGEN_TOKENIZERS.includes(textgen_settings.type) && (textgen_settings.type !== textgen_types.OOBA || hasValidEndpoint);
291293
292294 if (!hasTokenizerError && isConnected) {
293295 if (forApi === 'kobold' && kai_flags.can_use_tokenization) {
@@ -297,10 +299,10 @@ export function getTokenizerBestMatch(forApi) {
297299 if (forApi === 'textgenerationwebui' && isTokenizerSupported) {
298300 return tokenizers.API_TEXTGENERATIONWEBUI;
299301 }
300302 if (forApi === 'textgenerationwebui' && textgen_settings.type === textgen_types.OPENROUTER) {
301303 return getCurrentOpenRouterModelTokenizer();
302304 }
303305 if (forApi === 'textgenerationwebui' && textgen_settings.type === textgen_types.DREAMGEN) {
304306 return getCurrentDreamGenModelTokenizer();
305307 }
306308 }
@@ -576,7 +578,7 @@ export function getTokenizerModel() {
576578
577579 // And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer)
578580 if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model ||
579581 main_api == 'textgenerationwebui' && textgen_settings.type === textgen_types.OPENROUTER && textgen_settings.openrouter_model) {
580582 const model = main_api == 'openai'
581583 ? model_list.find(x => x.id === oai_settings.openrouter_model)
582584 : openRouterModels.find(x => x.id === textgen_settings.openrouter_model);
@@ -652,7 +654,7 @@ export function getTokenizerModel() {
652654 return oai_settings.custom_model;
653655 }
654656
655657 if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
656658 if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
657659 return llama3Tokenizer;
658660 }
@@ -680,7 +682,7 @@ export function getTokenizerModel() {
680682 return yiTokenizer;
681683 }
682684
683685 if (oai_settings.chat_completion_source === chat_completion_sources.BLOCKENTROPY) {
684686 if (oai_settings.blockentropy_model.includes('llama3')) {
685687 return llama3Tokenizer;
686688 }
@@ -1121,6 +1123,14 @@ export function decodeTextTokens(tokenizerType, ids) {
11211123}
11221124
11231125export async function initTokenizers() {
1126+ TEXTGEN_TOKENIZERS.push(
1127+ textgen_types.OOBA,
1128+ textgen_types.TABBY,
1129+ textgen_types.KOBOLDCPP,
1130+ textgen_types.LLAMACPP,
1131+ textgen_types.VLLM,
1132+ textgen_types.APHRODITE,
1133+ );
11241134 await loadTokenCache();
11251135 registerDebugFunction('resetTokenCache', 'Reset token cache', 'Purges the calculated token counts. Use this if you want to force a full re-tokenization of all chats or suspect the token counts are wrong.', resetTokenCache);
11261136}
public/scripts/user.js+1 -1
@@ -31,7 +31,7 @@ export async function setUserControls(isEnabled) {
3131 * Check if the current user is an admin.
3232 * @returns {boolean} True if the current user is an admin
3333 */
3434export function isAdmin() {
3535 if (!currentUser) {
3636 return false;
3737 }
public/scripts/utils.js+17 -0
@@ -2173,3 +2173,20 @@ export function getCharIndex(char) {
21732173 if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
21742174 return index;
21752175}
2176+
2177+/**
2178+ * Compares two arrays for equality
2179+ * @param {any[]} a - The first array
2180+ * @param {any[]} b - The second array
2181+ * @returns {boolean} True if the arrays are equal, false otherwise
2182+ */
2183+export function arraysEqual(a, b) {
2184+ if (a === b) return true;
2185+ if (a == null || b == null) return false;
2186+ if (a.length !== b.length) return false;
2187+
2188+ for (let i = 0; i < a.length; i++) {
2189+ if (a[i] !== b[i]) return false;
2190+ }
2191+ return true;
2192+}
public/scripts/world-info.js+72 -13
@@ -3548,6 +3548,11 @@ async function getCharacterLore() {
35483548 continue;
35493549 }
35503550
3551+ if (power_user.persona_description_lorebook === worldName) {
3552+ console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in persona lore! Skipping...`);
3553+ continue;
3554+ }
3555+
35513556 const data = await loadWorldInfo(worldName);
35523557 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
35533558 entries = entries.concat(newEntries);
@@ -3598,11 +3603,45 @@ async function getChatLore() {
35983603 return entries;
35993604}
36003605
3606+async function getPersonaLore() {
3607+ const chatWorld = chat_metadata[METADATA_KEY];
3608+ const personaWorld = power_user.persona_description_lorebook;
3609+
3610+ if (!personaWorld) {
3611+ return [];
3612+ }
3613+
3614+ if (chatWorld === personaWorld) {
3615+ console.debug(`[WI] Persona world ${personaWorld} is already activated in chat world! Skipping...`);
3616+ return [];
3617+ }
3618+
3619+ if (selected_world_info.includes(personaWorld)) {
3620+ console.debug(`[WI] Persona world ${personaWorld} is already activated in global world info! Skipping...`);
3621+ return [];
3622+ }
3623+
3624+ const data = await loadWorldInfo(personaWorld);
3625+ const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: personaWorld, ...rest })) : [];
3626+
3627+ console.debug(`[WI] Persona lore has ${entries.length} entries`, [personaWorld]);
3628+
3629+ return entries;
3630+}
3631+
36013632export async function getSortedEntries() {
36023633 try {
3603- const globalLore = await getGlobalLore();
3634+ const [
3604- const characterLore = await getCharacterLore();
3635+ globalLore,
3605- const chatLore = await getChatLore();
3636+ characterLore,
3637+ chatLore,
3638+ personaLore,
3639+ ] = await Promise.all([
3640+ getGlobalLore(),
3641+ getCharacterLore(),
3642+ getChatLore(),
3643+ getPersonaLore(),
3644+ ]);
36063645
36073646 let entries;
36083647
@@ -3622,8 +3661,8 @@ export async function getSortedEntries() {
36223661 break;
36233662 }
36243663
36253664 // Chat lore always goes first, then persona lore, then the rest
36263665 entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries];
36273666
36283667 // Calculate hash and parse decorators. Split maps to preserve old hashes.
36293668 entries = entries.map((entry) => {
@@ -4816,9 +4855,33 @@ export async function importWorldInfo(file) {
48164855 });
48174856}
48184857
4819-export function assignLorebookToChat() {
4858+/**
4859+ * Forces the world info editor to open on a specific world.
4860+ * @param {string} worldName The name of the world to open
4861+ */
4862+export function openWorldInfoEditor(worldName) {
4863+ console.log(`Opening lorebook for ${worldName}`);
4864+ if (!$('#WorldInfo').is(':visible')) {
4865+ $('#WIDrawerIcon').trigger('click');
4866+ }
4867+ const index = world_names.indexOf(worldName);
4868+ $('#world_editor_select').val(index).trigger('change');
4869+}
4870+
4871+/**
4872+ * Assigns a lorebook to the current chat.
4873+ * @param {PointerEvent} event Pointer event
4874+ * @returns {Promise<void>}
4875+ */
4876+export async function assignLorebookToChat(event) {
48204877 const selectedName = chat_metadata[METADATA_KEY];
4821- const template = $('#chat_world_template .chat_world').clone();
4878+
4879+ if (selectedName && event.altKey) {
4880+ openWorldInfoEditor(selectedName);
4881+ return;
4882+ }
4883+
4884+ const template = $(await renderTemplateAsync('chatLorebook'));
48224885
48234886 const worldSelect = template.find('select');
48244887 const chatName = template.find('.chat_name');
@@ -4846,7 +4909,7 @@ export function assignLorebookToChat() {
48464909 saveMetadata();
48474910 });
48484911
48494912 callPopupreturn callGenericPopup(template, 'text'POPUP_TYPE.TEXT);
48504913}
48514914
48524915jQuery(() => {
@@ -4997,11 +5060,7 @@ jQuery(() => {
49975060 const worldName = characters[chid]?.data?.extensions?.world;
49985061 const hasEmbed = checkEmbeddedWorld(chid);
49995062 if (worldName && world_names.includes(worldName) && !event.shiftKey) {
5000- if (!$('#WorldInfo').is(':visible')) {
5063+ openWorldInfoEditor(worldName);
5001- $('#WIDrawerIcon').trigger('click');
5002- }
5003- const index = world_names.indexOf(worldName);
5004- $('#world_editor_select').val(index).trigger('change');
50055064 } else if (hasEmbed && !event.shiftKey) {
50065065 await importEmbeddedWorldInfo();
50075066 saveCharacterDebounced();
public/style.css+12 -2
@@ -1040,8 +1040,14 @@ body .panelControlBar {
10401040 height: var(--swipeCounterHeight);
10411041}
10421042
10431043body:not(.swipeAllMessages) .mes:not(.last_mes) .swipes-counter {
10441044 opacityvisibility: 0.3hidden;
1045+}
1046+
1047+body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1048+ /* Avoid expensive DOM queries */
1049+ opacity: 0.3 !important;
1050+ display: flex !important;
10451051}
10461052
10471053.swipe_left {
@@ -2782,6 +2788,10 @@ select option:not(:checked) {
27822788 width: max-content;
27832789}
27842790
2791+#persona-management-block .menu_button {
2792+ filter: grayscale(0.5);
2793+}
2794+
27852795input[type=search]::-webkit-search-cancel-button {
27862796 -webkit-appearance: none;
27872797 height: 1em;
src/constants.js+1 -0
@@ -3,6 +3,7 @@ export const PUBLIC_DIRECTORIES = {
33 backups: 'backups/',
44 sounds: 'public/sounds',
55 extensions: 'public/scripts/extensions',
6+ globalExtensions: 'public/scripts/extensions/third-party',
67};
78
89export const SETTINGS_FILE = 'settings.json';
src/endpoints/backends/chat-completions.js+7 -1
@@ -280,7 +280,13 @@ async function sendMakerSuiteRequest(request, response) {
280280 delete generationConfig.stopSequences;
281281 }
282282
283- const should_use_system_prompt = (model.includes('gemini-1.5-flash') || model.includes('gemini-1.5-pro') || model.includes('gemini-exp-1114') || model.includes('gemini-exp-1121')) && request.body.use_makersuite_sysprompt;
283+ const should_use_system_prompt = (
284+ model.includes('gemini-2.0-flash-exp') ||
285+ model.includes('gemini-1.5-flash') ||
286+ model.includes('gemini-1.5-pro') ||
287+ model.startsWith('gemini-exp')
288+ ) && request.body.use_makersuite_sysprompt;
289+
284290 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, request.body.char_name, request.body.user_name);
285291 let body = {
286292 contents: prompt.contents,
src/endpoints/characters.js+2 -1
@@ -158,7 +158,8 @@ async function tryReadImage(imgPath, crop) {
158158 return image;
159159 }
160160 // If it's an unsupported type of image (APNG) - just read the file as buffer
161161 catch (error) {
162+ console.log(`Failed to read image: ${imgPath}`, error);
162163 return fs.readFileSync(imgPath);
163164 }
164165}
src/endpoints/chats.js+27 -22
@@ -9,7 +9,18 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
99import _ from 'lodash';
1010
1111import { jsonParser, urlencodedParser } from '../express-common.js';
12-import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } from '../util.js';
12+import {
13+ getConfigValue,
14+ humanizedISO8601DateTime,
15+ tryParse,
16+ generateTimestamp,
17+ removeOldBackups,
18+ formatBytes,
19+} from '../util.js';
20+
21+const isBackupDisabled = getConfigValue('disableChatBackup', false);
22+const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', -1));
23+const throttleInterval = getConfigValue('chatBackupThrottleInterval', 10_000);
1324
1425/**
1526 * Saves a chat to the backups directory.
@@ -19,7 +30,6 @@ import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp,
1930 */
2031function backupChat(directory, name, chat) {
2132 try {
22- const isBackupDisabled = getConfigValue('disableChatBackup', false);
2333
2434 if (isBackupDisabled) {
2535 return;
@@ -32,11 +42,20 @@ function backupChat(directory, name, chat) {
3242 writeFileAtomicSync(backupFile, chat, 'utf-8');
3343
3444 removeOldBackups(directory, `chat_${name}_`);
45+
46+ if (isNaN(maxTotalChatBackups) || maxTotalChatBackups < 0) {
47+ return;
48+ }
49+
50+ removeOldBackups(directory, 'chat_', maxTotalChatBackups);
3551 } catch (err) {
3652 console.log(`Could not backup chat for ${name}`, err);
3753 }
3854}
3955
56+/**
57+ * @type {Map<string, import('lodash').DebouncedFunc<function(string, string, string): void>>}
58+ */
4059const backupFunctions = new Map();
4160
4261/**
@@ -45,24 +64,10 @@ const backupFunctions = new Map();
4564 * @returns {function(string, string, string): void} Backup function
4665 */
4766function getBackupFunction(handle) {
48- const throttleInterval = getConfigValue('chatBackupThrottleInterval', 10_000);
4967 if (!backupFunctions.has(handle)) {
5068 backupFunctions.set(handle, _.throttle(backupChat, throttleInterval, { leading: true, trailing: true }));
5169 }
5270 return backupFunctions.get(handle) || (() => {});
53-}
54-
55-/**
56- * Formats a byte size into a human-readable string with units
57- * @param {number} bytes - The size in bytes to format
58- * @returns {string} The formatted string (e.g., "1.5 MB")
59- */
60-function formatBytes(bytes) {
61- if (bytes === 0) return '0 B';
62- const k = 1024;
63- const sizes = ['B', 'KB', 'MB', 'GB'];
64- const i = Math.floor(Math.log(bytes) / Math.log(k));
65- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
6671}
6772
6873/**
@@ -203,19 +208,19 @@ function importKoboldLiteChat(_userName, _characterName, data) {
203208
204209 /** @type {function(string): object} */
205210 function processKoboldMessage(msg) {
206211 const isUser = msg.includes(inputToken) || msg.includes(outputToken);
207212 return {
208213 name: isUser ? header.user_name : header.character_name,
209214 is_user: isUser,
210215 mes: msg.replacereplaceAll(inputToken, '').replacereplaceAll(outputToken, '').trim(),
211216 send_date: Date.now(),
212217 };
213218 }
214219
215220 // Create the header
216221 const header = {
217222 user_name: String(data.savedsettings.chatname),
218223 character_name: String(data.savedsettings.chatopponent).split('||$||')[0],
219224 };
220225 // Format messages
221226 const formattedMessages = data.actions.map(processKoboldMessage);
@@ -702,7 +707,7 @@ router.post('/search', jsonParser, function (request, response) {
702707 }
703708
704709 // Sort by last message date descending
705710 results.sort((a, b) => new Date(b.last_mes).getTime() - new Date(a.last_mes).getTime());
706711 return response.send(results);
707712
708713 } catch (error) {
src/endpoints/extensions.js+117 -32
@@ -73,8 +73,19 @@ router.post('/install', jsonParser, async (request, response) => {
7373 fs.mkdirSync(path.join(request.user.directories.extensions));
7474 }
7575
76- const url = request.body.url;
76+ if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) {
77- const extensionPath = path.join(request.user.directories.extensions, path.basename(url, '.git'));
77+ fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);
78+ }
79+
80+ const { url, global } = request.body;
81+
82+ if (global && !request.user.profile.admin) {
83+ console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
84+ return response.status(403).send('Forbidden: No permission to install global extensions.');
85+ }
86+
87+ const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
88+ const extensionPath = path.join(basePath, sanitize(path.basename(url, '.git')));
7889
7990 if (fs.existsSync(extensionPath)) {
8091 return response.status(409).send(`Directory already exists at ${extensionPath}`);
@@ -83,10 +94,8 @@ router.post('/install', jsonParser, async (request, response) => {
8394 await git.clone(url, extensionPath, { '--depth': 1 });
8495 console.log(`Extension has been cloned at ${extensionPath}`);
8596
86-
8797 const { version, author, display_name } = await getManifest(extensionPath);
8898
89-
9099 return response.send({ version, author, display_name, extensionPath });
91100 } catch (error) {
92101 console.log('Importing custom content failed', error);
@@ -112,8 +121,15 @@ router.post('/update', jsonParser, async (request, response) => {
112121 }
113122
114123 try {
115124 const { extensionName, global } = request.body.extensionName;
116- const extensionPath = path.join(request.user.directories.extensions, extensionName);
125+
126+ if (global && !request.user.profile.admin) {
127+ console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
128+ return response.status(403).send('Forbidden: No permission to update global extensions.');
129+ }
130+
131+ const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
132+ const extensionPath = path.join(basePath, extensionName);
117133
118134 if (!fs.existsSync(extensionPath)) {
119135 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
@@ -122,7 +138,6 @@ router.post('/update', jsonParser, async (request, response) => {
122138 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
123139 const currentBranch = await git.cwd(extensionPath).branch();
124140 if (!isUpToDate) {
125-
126141 await git.cwd(extensionPath).pull('origin', currentBranch.current);
127142 console.log(`Extension has been updated at ${extensionPath}`);
128143 } else {
@@ -140,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => {
140155 }
141156});
142157
158+router.post('/move', jsonParser, async (request, response) => {
159+ try {
160+ const { extensionName, source, destination } = request.body;
161+
162+ if (!extensionName || !source || !destination) {
163+ return response.status(400).send('Bad Request. Not all required parameters are provided.');
164+ }
165+
166+ if (!request.user.profile.admin) {
167+ console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`);
168+ return response.status(403).send('Forbidden: No permission to move extensions.');
169+ }
170+
171+ const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
172+ const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
173+ const sourcePath = path.join(sourceDirectory, sanitize(extensionName));
174+ const destinationPath = path.join(destinationDirectory, sanitize(extensionName));
175+
176+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) {
177+ console.error(`Source directory does not exist at ${sourcePath}`);
178+ return response.status(404).send('Source directory does not exist.');
179+ }
180+
181+ if (fs.existsSync(destinationPath)) {
182+ console.error(`Destination directory already exists at ${destinationPath}`);
183+ return response.status(409).send('Destination directory already exists.');
184+ }
185+
186+ if (source === destination) {
187+ console.error('Source and destination directories are the same');
188+ return response.status(409).send('Source and destination directories are the same.');
189+ }
190+
191+ fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true });
192+ fs.rmSync(sourcePath, { recursive: true, force: true });
193+ console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`);
194+
195+ return response.sendStatus(204);
196+ } catch (error) {
197+ console.log('Moving extension failed', error);
198+ return response.status(500).send('Internal Server Error. Try again later.');
199+ }
200+});
201+
143202/**
144203 * HTTP POST handler function to get the current git commit hash and branch name for a given extension.
145204 * It checks whether the repository is up-to-date with the remote, and returns the status along with
@@ -157,19 +216,28 @@ router.post('/version', jsonParser, async (request, response) => {
157216 }
158217
159218 try {
160219 const { extensionName, global } = request.body.extensionName;
161220 const extensionPathbasePath = pathglobal ? PUBLIC_DIRECTORIES.join(globalExtensions : request.user.directories.extensions, extensionName);
221+ const extensionPath = path.join(basePath, sanitize(extensionName));
162222
163223 if (!fs.existsSync(extensionPath)) {
164224 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
165225 }
166226
227+ let currentCommitHash;
228+ try {
229+ currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
230+ } catch (error) {
231+ // it is not a git repo, or has no commits yet, or is a bare repo
232+ // not possible to update it, most likely can't get the branch name either
233+ return response.send({ currentBranchName: null, currentCommitHash, isUpToDate: true, remoteUrl: null });
234+ }
235+
167236 const currentBranch = await git.cwd(extensionPath).branch();
168237 // get only the working branch
169238 const currentBranchName = currentBranch.current;
170239 await git.cwd(extensionPath).fetch('origin');
171- const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
240+ console.log(extensionName, currentBranchName, currentCommitHash);
172- console.log(currentBranch, currentCommitHash);
173241 const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
174242
175243 return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
@@ -193,11 +261,16 @@ router.post('/delete', jsonParser, async (request, response) => {
193261 return response.status(400).send('Bad Request: extensionName is required in the request body.');
194262 }
195263
196- // Sanitize the extension name to prevent directory traversal
197- const extensionName = sanitize(request.body.extensionName);
198-
199264 try {
200- const extensionPath = path.join(request.user.directories.extensions, extensionName);
265+ const { extensionName, global } = request.body;
266+
267+ if (global && !request.user.profile.admin) {
268+ console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
269+ return response.status(403).send('Forbidden: No permission to delete global extensions.');
270+ }
271+
272+ const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions;
273+ const extensionPath = path.join(basePath, sanitize(extensionName));
201274
202275 if (!fs.existsSync(extensionPath)) {
203276 return response.status(404).send(`Directory does not exist at ${extensionPath}`);
@@ -219,26 +292,38 @@ router.post('/delete', jsonParser, async (request, response) => {
219292 * If the folder is called third-party, search for subfolders instead
220293 */
221294router.get('/discover', jsonParser, function (request, response) {
222- // get all folders in the extensions folder, except third-party
223- const extensions = fs
224- .readdirSync(PUBLIC_DIRECTORIES.extensions)
225- .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory())
226- .filter(f => f !== 'third-party');
227-
228- // get all folders in the third-party folder, if it exists
229-
230295 if (!fs.existsSync(path.join(request.user.directories.extensions))) {
231- return response.send(extensions);
296+ fs.mkdirSync(path.join(request.user.directories.extensions));
232297 }
233298
234- const thirdPartyExtensions = fs
299+ if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) {
235- .readdirSync(path.join(request.user.directories.extensions))
300+ fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions);
236- .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory());
301+ }
237-
238- // add the third-party extensions to the extensions array
239- extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
240- console.log(extensions);
241302
303+ // Get all folders in system extensions folder, excluding third-party
304+ const builtInExtensions = fs
305+ .readdirSync(PUBLIC_DIRECTORIES.extensions)
306+ .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory())
307+ .filter(f => f !== 'third-party')
308+ .map(f => ({ type: 'system', name: f }));
242309
243- return response.send(extensions);
310+ // Get all folders in local extensions folder
311+ const userExtensions = fs
312+ .readdirSync(path.join(request.user.directories.extensions))
313+ .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory())
314+ .map(f => ({ type: 'local', name: `third-party/${f}` }));
315+
316+ // Get all folders in global extensions folder
317+ // In case of a conflict, the extension will be loaded from the user folder
318+ const globalExtensions = fs
319+ .readdirSync(PUBLIC_DIRECTORIES.globalExtensions)
320+ .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory())
321+ .map(f => ({ type: 'global', name: `third-party/${f}` }))
322+ .filter(f => !userExtensions.some(e => e.name === f.name));
323+
324+ // Combine all extensions
325+ const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions];
326+ console.log('Extensions available for', request.user.profile.handle, allExtensions);
327+
328+ return response.send(allExtensions);
244329});
src/prompt-converters.js+2 -0
@@ -337,6 +337,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
337337 const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
338338
339339 const visionSupportedModels = [
340+ 'gemini-2.0-flash-exp',
340341 'gemini-1.5-flash',
341342 'gemini-1.5-flash-latest',
342343 'gemini-1.5-flash-001',
@@ -347,6 +348,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
347348 'gemini-1.5-flash-8b-exp-0924',
348349 'gemini-exp-1114',
349350 'gemini-exp-1121',
351+ 'gemini-exp-1206',
350352 'gemini-1.5-pro',
351353 'gemini-1.5-pro-latest',
352354 'gemini-1.5-pro-001',
src/users.js+29 -1
@@ -783,6 +783,34 @@ function createRouteHandler(directoryFn) {
783783}
784784
785785/**
786+ * Creates a route handler for serving extensions.
787+ * @param {(req: import('express').Request) => string} directoryFn A function that returns the directory path to serve files from
788+ * @returns {import('express').RequestHandler}
789+ */
790+function createExtensionsRouteHandler(directoryFn) {
791+ return async (req, res) => {
792+ try {
793+ const directory = directoryFn(req);
794+ const filePath = decodeURIComponent(req.params[0]);
795+
796+ const existsLocal = fs.existsSync(path.join(directory, filePath));
797+ if (existsLocal) {
798+ return res.sendFile(filePath, { root: directory });
799+ }
800+
801+ const existsGlobal = fs.existsSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, filePath));
802+ if (existsGlobal) {
803+ return res.sendFile(filePath, { root: PUBLIC_DIRECTORIES.globalExtensions });
804+ }
805+
806+ return res.sendStatus(404);
807+ } catch (error) {
808+ return res.sendStatus(500);
809+ }
810+ };
811+}
812+
813+/**
786814 * Verifies that the current user is an admin.
787815 * @param {import('express').Request} request Request object
788816 * @param {import('express').Response} response Response object
@@ -872,4 +900,4 @@ router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.a
872900router.use('/assets/*', createRouteHandler(req => req.user.directories.assets));
873901router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages));
874902router.use('/user/files/*', createRouteHandler(req => req.user.directories.files));
875903router.use('/scripts/extensions/third-party/*', createRouteHandlercreateExtensionsRouteHandler(req => req.user.directories.extensions));
src/util.js+24 -3
@@ -143,6 +143,19 @@ export function getHexString(length) {
143143}
144144
145145/**
146+ * Formats a byte size into a human-readable string with units
147+ * @param {number} bytes - The size in bytes to format
148+ * @returns {string} The formatted string (e.g., "1.5 MB")
149+ */
150+export function formatBytes(bytes) {
151+ if (bytes === 0) return '0 B';
152+ const k = 1024;
153+ const sizes = ['B', 'KB', 'MB', 'GB'];
154+ const i = Math.floor(Math.log(bytes) / Math.log(k));
155+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
156+}
157+
158+/**
146159 * Extracts a file with given extension from an ArrayBuffer containing a ZIP archive.
147160 * @param {ArrayBuffer} archiveBuffer Buffer containing a ZIP archive
148161 * @param {string} fileExtension File extension to look for
@@ -376,16 +389,24 @@ export function generateTimestamp() {
376389 * Remove old backups with the given prefix from a specified directory.
377390 * @param {string} directory The root directory to remove backups from.
378391 * @param {string} prefix File prefix to filter backups by.
392+ * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `numberOfBackups` config value.
379393 */
380394export function removeOldBackups(directory, prefix, limit = null) {
381395 const MAX_BACKUPS = limit ?? Number(getConfigValue('numberOfBackups', 50));
382396
383397 let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
384398 if (files.length > MAX_BACKUPS) {
385399 files = files.map(f => path.join(directory, f));
386400 files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
387401
388- fs.rmSync(files[0]);
402+ while (files.length > MAX_BACKUPS) {
403+ const oldest = files.shift();
404+ if (!oldest) {
405+ break;
406+ }
407+
408+ fs.rmSync(oldest);
409+ }
389410 }
390411}
391412