Merge branch 'staging' into generate-cleanups-3

This commit is contained in:
Cohee
2024-01-01 16:49:35 +02:00
18 changed files with 495 additions and 30 deletions

View File

@ -9,3 +9,6 @@ trim_trailing_whitespace = true
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.md]
trim_trailing_whitespace = false

314
.github/readme-ja_jp.md vendored Normal file
View File

@ -0,0 +1,314 @@
[English](readme.md) | [中文](readme-zh_cn.md) | 日本語
![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
モバイルフレンドリーなレイアウト、マルチAPIKoboldAI/CPP、Horde、NovelAI、Ooba、OpenAI、OpenRouter、Claude、Scale、VN ライクな Waifu モード、Stable Diffusion、TTS、WorldInfo伝承本、カスタマイズ可能な UI、自動翻訳、あなたにとって必要とする以上のプロンプトオプションサードパーティの拡張機能をインストールする機能。
[TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 のフォークに基づいています
## 重要ニュース!
1. 私たちは[ドキュメント website](https://docs.sillytavern.app/) を作成し、ほとんどの質問にお答えしています。
2. アップデートしたらに拡張機能を見失った?リリースバージョン 1.10.6 以降、これまで内蔵されていた拡張機能のほとんどがダウンロード可能なアドオンに変更されました。ダウンロードは、拡張機能パネル(トップバーのスタックドブロックアイコン)にある内蔵の "Download Extensions and Assets" メニューから行えます。
### Cohee、RossAscends、SillyTavern コミュニティがお届けします
### SillyTavern または TavernAI とは何ですか?
SillyTavern は、あなたのコンピュータ(および Android スマホ)にインストールできるユーザーインターフェイスで、テキスト生成 AI と対話したり、あなたやコミュニティが作成したキャラクターとチャットやロールプレイをすることができます。
SillyTavern は TavernAI 1.2.8 のフォークで、より活発な開発が行われており、多くの主要な機能が追加されています。現時点では、これらは完全に独立したプログラムと考えることができます。
### ブランチ
SillyTavern は、すべてのユーザーにスムーズな体験を保証するために、2 つのブランチシステムを使用して開発されています。
* release -🌟 **ほとんどのユーザーにお勧め。** これは最も安定した推奨ブランチで、メジャーリリースがプッシュされた時のみ更新されます。大半のユーザーに適しています。
* staging - ⚠️ **カジュアルな使用にはお勧めしない。** このブランチには最新の機能がありますが、いつ壊れるかわからないので注意してください。パワーユーザーとマニア向けです。
git CLI の使い方に慣れていなかったり、ブランチが何なのかわからなかったりしても、心配はいりません!リリースブランチが常に望ましい選択肢となります。
### Tavern 以外に何が必要ですか?
Tavern は単なるユーザーインターフェイスなので、それだけでは役に立ちません。ロールプレイキャラクターとして機能する AI システムのバックエンドにアクセスする必要があります。様々なバックエンドがサポートされています: OpenAPI API (GPT)、KoboldAI (ローカルまたは Google Colab 上で動作)、その他。詳しくは [FAQ](https://docs.sillytavern.app/usage/faq/) をご覧ください。
### Tavern を実行するには、強力な PC が必要ですか?
Tavern は単なるユーザーインターフェイスであり、必要なハードウェアはごくわずかです。パワフルである必要があるのは、AI システムのバックエンドです。
## モバイルサポート
> **注**
> **このフォークは Termux を使って Android スマホでネイティブに実行できます。ArroganceComplex#2659 のガイドを参照してください:**
<https://rentry.org/STAI-Termux>
## ご質問やご提案
### コミュニティ Discord サーバーを開設しました
サポートを受け、お気に入りのキャラクターやプロンプトを共有する:
### [参加](https://discord.gg/RZdyAEUPvj)
***
開発者と直接連絡を取る:
* Discord: cohee または rossascends
* Reddit: /u/RossAscends または /u/sillylossy
* [GitHub issue を投稿](https://github.com/SillyTavern/SillyTavern/issues)
## このバージョンには以下が含まれる
* 大幅に修正された TavernAI 1.2.8 (コードの 50% 以上が書き換えまたは最適化されています)
* スワイプ
* グループチャット: キャラクター同士が会話できるマルチボットルーム
* チャットチェックポイント / ブランチ
* 高度なKoboldAI / TextGen生成設定と、コミュニティが作成した多くのプリセット
* ワールド情報サポート: 豊富な伝承を作成したり、キャラクターカードにトークンを保存したりできます
* [OpenRouter](https://openrouter.ai) 各種 API(Claude、GPT-4/3.5 など)の接続
* [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui) API 接続
* [AI Horde](https://horde.koboldai.net/) 接続
* プロンプト生成フォーマットの調整
## 拡張機能
SillyTavern は拡張性をサポートしており、[SillyTavern Extras API](https://github.com/SillyTavern/SillyTavern-extras) を介していくつかの追加AIモジュールをホストしています
* 作者ノート/キャラクターバイアス
* キャラクターの感情表現(スプライト)
* チャット履歴の自動サマリー
* チャットに画像を送り、AI が内容を解釈する
* Stable Diffusion 画像生成 (5 つのチャット関連プリセットと 'free mode')
* AI 応答メッセージの音声合成ElevenLabs、Silero、または OS のシステム TTS 経由)
含まれている拡張機能の完全なリストとその使い方のチュートリアルは [Docs](https://docs.sillytavern.app/) にあります。
## RossAscends による UI/CSS/クオリティオブライフの調整
* iOS 用に最適化されたモバイル UI で、ホーム画面へのショートカット保存とフルスクリーンモードでの起動をサポート。
* ホットキー
* Up = チャットの最後のメッセージを編集する
* Ctrl+Up = チャットで最後のユーザーメッセージを編集する
* Left = 左スワイプ
* Right = 右スワイプ (注: チャットバーに何か入力されている場合、スワイプホットキーが無効になります)
* Ctrl+Left = ローカルに保存された変数を見る(ブラウザのコンソールウィンドウにて)
* Enter (チャットバー選択時) = AI にメッセージを送る
* Ctrl+Enter = 最後の AI 応答を再生成する
* ユーザー名の変更と文字の削除でページが更新されなくなりました。
* ページロード時に API に自動的に接続するかどうかを切り替えます。
* ページの読み込み時に、最近見た文字を自動的に読み込むかどうかを切り替えます。
* より良いトークンカウンター - 保存されていないキャラクターに対して機能し、永続的なトークンと一時的なトークンの両方を表示する。
* より良い過去のチャット
* 新しいチャットのファイル名は、"(文字) - (作成日)" という読みやすい形式で保存されます
* チャットのプレビューが 40 文字から 300 文字に増加。
* 文字リストの並べ替えに複数のオプション(名前順、作成日順、チャットサイズ順)があります。
* デフォルトでは、左右の設定パネルはクリックすると閉じます。
* ナビパネルのロックをクリックすると、パネルが開いたままになり、この設定はセッションをまたいで記憶されます。
* ナビパネルの開閉状態もセッションをまたいで保存されます。
* カスタマイズ可能なチャット UI:
* 新しいメッセージが届いたときにサウンドを再生する
* 丸型、長方形のアバタースタイルの切り替え
* デスクトップのチャットウィンドウを広くする
* オプションの半透明ガラス風パネル
* 'メインテキスト'、'引用テキスト'、'斜体テキスト'のページカラーをカスタマイズ可能。
* カスタマイズ可能な UI 背景色とぼかし量
## インストール
*注: このソフトウェアはローカルにインストールすることを目的としており、colab や他のクラウドノートブックサービス上では十分にテストされていません。*
> **警告**
> WINDOWS が管理しているフォルダProgram Files、System32 など)にはインストールしないでください
> START.BAT を管理者権限で実行しないでください
### Windows
Git 経由でのインストール(更新を容易にするため推奨)
きれいな写真付きのわかりやすいガイド:
<https://docs.sillytavern.app/installation/windows/>
1. [NodeJS](https://nodejs.org/en) をインストールする(最新の LTS 版を推奨)
2. [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32) をインストールする
3. Windows エクスプローラーを開く (`Win+E`)
4. Windows によって制御または監視されていないフォルダを参照または作成する。(例: C:\MySpecialFolder\
5. 上部のアドレスバーをクリックし、`cmd` と入力して Enter キーを押し、そのフォルダーの中にコマンドプロンプトを開きます。
6. 黒いボックスコマンドプロンプトがポップアップしたら、そこに以下のいずれかを入力し、Enter を押します:
* Release ブランチの場合: `git clone https://github.com/SillyTavern/SillyTavern -b release`
* Staging ブランチの場合: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
7. すべてをクローンしたら、`Start.bat` をダブルクリックして、NodeJS に要件をインストールさせる。
8. サーバーが起動し、SillyTavern がブラウザにポップアップ表示されます。
ZIP ダウンロードによるインストール(推奨しない)
1. [NodeJS](https://nodejs.org/en) をインストールする(最新の LTS 版を推奨)
2. GitHub のリポジトリから zip をダウンロードする。(`ソースコード(zip)` は [Releases](https://github.com/SillyTavern/SillyTavern/releases/latest) から入手)
3. お好きなフォルダに解凍してください
4. `Start.bat` をダブルクリックまたはコマンドラインで実行する。
5. サーバーがあなたのためにすべてを準備したら、ブラウザのタブを開きます。
### Linux
1. `node -v` を実行して、Node.js v18 以上(最新の [LTS バージョン](https://nodejs.org/en/download/) を推奨)がインストールされていることを確認してください。
または、[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating) スクリプトを使用して、迅速かつ簡単に Node のインストールを管理します。
2. `start.sh` スクリプトを実行する。
3. お楽しみください。
## API キー管理
SillyTavern は API キーをサーバーディレクトリの `secrets.json` ファイルに保存します。
デフォルトでは、入力後にページをリロードしても、フロントエンドには表示されません。
API ブロックのボタンをクリックして、キーを閲覧できるようにする:
1. ファイル `config.yaml``allowKeysExposure` の値を `true` に設定する。
2. SillyTavern サーバを再起動します。
## リモート接続
SillyTavern をスマホで使用しながら、同じ Wifi ネットワーク上で ST サーバーを PC で実行したい場合に使用します。
しかし、これはどこからでもリモート接続を許可するために使用することができます。
**重要: SillyTavern はシングルユーザーのプログラムなので、ログインすれば誰でもすべてのキャラクターとチャットを見ることができ、UI 内で設定を変更することができます。**
### 1. ホワイトリスト IP の管理
* SillyTavern のベースインストールフォルダ内に `whitelist.txt` という新しいテキストファイルを作成します。
* テキストエディタでこのファイルを開き、接続を許可したい IP のリストを追加します。
*個々の IP とワイルドカード IP 範囲の両方が受け入れられる。例:*
```txt
192.168.0.1
192.168.0.20
```
または
```txt
192.168.0.*
```
(上記のワイルドカード IP 範囲は、ローカルネットワーク上のどのデバイスでも)
CIDR マスクも受け付ける10.0.0.0/24
* `whitelist.txt` ファイルを保存する。
* TAI サーバーを再起動する。
これでファイルに指定された IP を持つデバイスが接続できるようになる。
*注: `config.yaml` にも `whitelist` 配列があり、同じように使うことができるが、`whitelist.txt` が存在する場合、この配列は無視される。*
### 2. ST ホストマシンの IP の取得
ホワイトリストの設定後、ST ホストデバイスの IP が必要になります。
ST ホストデバイスが同じ無線 LAN ネットワーク上にある場合、ST ホストの内部無線 LAN IP を使用します:
* Windows の場合: ウィンドウズボタン > 検索バーに `cmd.exe` と入力 > コンソールに `ipconfig` と入力して Enter > `IPv4` のリストを探す。
同じネットワーク上にいない状態で、ホストしているSTに接続したい場合は、STホスト機器のパブリックIPが必要です。
* ST ホストデバイスを使用中に、[このページ](https://whatismyipaddress.com/)にアクセスし、`IPv4` を探してください。これはリモートデバイスからの接続に使用するものです。
### 3. リモートデバイスを ST ホストマシンに接続します。
最終的に使用する IP が何であれ、その IP アドレスとポート番号をリモートデバイスのウェブブラウザに入力します。
同じ無線 LAN ネットワーク上の ST ホストの典型的なアドレスは以下のようになります:
`http://192.168.0.5:8000`
http:// を使用し、https:// は使用しないでください
### ST をすべての IP に開放する
これはお勧めしませんが、`config.yaml` を開き、`whitelistMode``false` に変更してください。
SillyTavern のベースインストールフォルダにある `whitelist.txt` が存在する場合は削除(または名前の変更)する必要があります。
これは通常安全ではないので、これを行う際にはユーザー名とパスワードを設定する必要があります。
ユーザー名とパスワードは `config.yaml` で設定します。
ST サーバを再起動すると、ユーザ名とパスワードさえ知っていれば、IP に関係なくどのデバイスでも ST サーバに接続できるようになる。
### まだ接続できませんか?
* `config.yaml` で見つかったポートに対して、インバウンド/アウトバウンドのファイアウォールルールを作成します。これをルーターのポートフォワーディングと間違えないでください。そうしないと、誰かがあなたのチャットログを見つける可能性があり、それはマジで止めましょう。
* 設定 > ネットワークとインターネット > イーサネットで、プライベートネットワークのプロファイルタイプを有効にします。そうしないと、前述のファイアウォールルールを使っても接続できません。
## パフォーマンスに問題がありますか?
ユーザー設定パネルでブラー効果なし(高速 UIモードを有効にしてみてください。
## このプロジェクトが好きです!どうすればコントリビュートできますか?
### やるべきこと
1. プルリクエストを送る
2. 確立されたテンプレートを使って機能提案と課題レポートを送る
3. 何か質問する前に、readme ファイルや組み込みのドキュメントを読んでください
### やらないべきこと
1. 金銭の寄付を申し出る
2. 何の脈絡もなくバグ報告を送る
3. すでに何度も回答されている質問をする
## 古い背景画像はどこにありますか?
100 オリジナルコンテンツのみのポリシーに移行しているため、古い背景画像はこのリポジトリから削除されました。
アーカイブはこちら:
<https://files.catbox.moe/1xevnc.zip>
## スクリーンショット
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649245-8061c60f-63dc-488e-9325-f151b7a3ec2d.png">
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649856-fbdeef05-d727-4d5a-be80-266cbbc6b811.png">
## ライセンスとクレジット
**このプログラムは有用であることを願って配布されていますが、いかなる保証もありません;
また、商品性または特定目的への適合性についての黙示の保証もありません。
詳細は GNU Affero General Public License をご覧ください。**
* Humi によるTAI Base: 不明ライセンス
* Cohee の修正と派生コード: AGPL v3
* RossAscends の追加: AGPL v3
* CncAnon の TavernAITurbo 改造の一部: 不明ライセンス
* kingbri のさまざまなコミットと提案 (<https://github.com/bdashore3>)
* city_unit の拡張機能と様々な QoL 機能 (<https://github.com/city-unit>)
* StefanDanielSchwarz のさまざまなコミットとバグ報告 (<https://github.com/StefanDanielSchwarz>)
* PepperTaco の作品にインスパイアされた Waifu モード (<https:/fugithub.com/peppertaco/Tavern/>)
* ピグマリオン大学の皆さん、素晴らしいテスターとしてクールな機能を提案してくれてありがとう!
* TextGen のプリセットをコンパイルしてくれた obabooga に感謝
* KAI Lite の KoboldAI プリセット: <https://lite.koboldai.net/>
* Google による Noto Sans フォントOFLライセンス
* Font Awesome によるアイコンテーマ <https://fontawesome.com> (アイコン: CC BY 4.0、フォント: SIL OFL 1.1、コード: MIT License)
* ZeldaFan0225 による AI Horde クライアントライブラリ: <https://github.com/ZeldaFan0225/ai_horde>
* AlpinDale による Linux 起動スクリプト
* FAQ を提供してくれた paniphons に感謝
* 10K ディスコード・ユーザー記念背景 by @kallmeflocc
* デフォルトコンテンツ(キャラクターと伝承書)の提供: @OtisAlejandro@RossAscends@kallmeflocc
* @doloroushyeonse による韓国語翻訳
* k_euler_a による Horde のサポート <https://github.com/Teashrock>
* [@XXpE3](https://github.com/XXpE3) による中国語翻訳、中国語 ISSUES の連絡先は @XXpE3

View File

@ -1,4 +1,4 @@
[English](readme.md) | 中文 [English](readme.md) | 中文 | [日本語](readme-ja_jp.md)
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4) ![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
@ -82,7 +82,7 @@ SillyTavern 支持扩展服务,一些额外的人工智能模块可通过 [Sil
* 文本图像生成5 预设,以及 "自由模式" * 文本图像生成5 预设,以及 "自由模式"
* 聊天信息的文字转语音(通过 ElevenLabs、Silero 或操作系统的语音生成) * 聊天信息的文字转语音(通过 ElevenLabs、Silero 或操作系统的语音生成)
扩展服务的完整功能介绍和使用教程,请参阅 [Docs](https://docs.sillytavern.app/extras/extensions/)。 扩展服务的完整功能介绍和使用教程,请参阅 [Docs](https://docs.sillytavern.app/)。
## 界面/CSS/性能,由 RossAscends 调整并优化 ## 界面/CSS/性能,由 RossAscends 调整并优化

15
.github/readme.md vendored
View File

@ -1,4 +1,4 @@
English | [中文](readme-zh_cn.md) English | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md)
![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4) ![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
@ -85,7 +85,7 @@ SillyTavern has extensibility support, with some additional AI modules hosted vi
* Stable Diffusion image generation (5 chat-related presets plus 'free mode') * Stable Diffusion image generation (5 chat-related presets plus 'free mode')
* Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS) * Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS)
A full list of included extensions and tutorials on how to use them can be found in the [Docs](https://docs.sillytavern.app/extras/extensions/). A full list of included extensions and tutorials on how to use them can be found in the [Docs](https://docs.sillytavern.app/).
## UI/CSS/Quality of Life tweaks by RossAscends ## UI/CSS/Quality of Life tweaks by RossAscends
@ -162,6 +162,17 @@ Installing via ZIP download (discouraged)
### Linux ### Linux
#### Unofficial Debian/Ubuntu PKGBUILD
> **This installation method is unofficial and not supported by the project. Report any issues to the PKGBUILD maintainer.**
> The method is intended for Debian-based distributions (Ubuntu, Mint, etc).
1. Install [makedeb](https://www.makedeb.org/).
2. Ensure you have Node.js v18 or higher installed by running `node -v`. If you need to upgrade, you can install a [node.js repo](https://mpr.makedeb.org/packages/nodejs-repo) (you'll might need to edit the version inside the PKGBUILD). As an alternative, install and configure [nvm](https://mpr.makedeb.org/packages/nvm) to manage multiple node.js installations. Finally, you can [install node.js manually](https://nodejs.org/en/download), but you will need to update the PATH variable of your environment.
3. Now build the [sillytavern package](https://mpr.makedeb.org/packages/sillytavern). The build needs to run with the correct node.js version.
#### Manual
1. Ensure you have Node.js v18 or higher (the latest [LTS version](https://nodejs.org/en/download/) is recommended) installed by running `node -v`. 1. Ensure you have Node.js v18 or higher (the latest [LTS version](https://nodejs.org/en/download/) is recommended) installed by running `node -v`.
Alternatively, use the [Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating) script to quickly and easily manage your Node installations. Alternatively, use the [Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating) script to quickly and easily manage your Node installations.
2. Run the `start.sh` script. 2. Run the `start.sh` script.

View File

@ -51,7 +51,7 @@ extras:
# Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format
classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
captioningModel: Xenova/vit-gpt2-image-captioning captioningModel: Xenova/vit-gpt2-image-captioning
embeddingModel: Xenova/all-mpnet-base-v2 embeddingModel: Cohee/jina-embeddings-v2-base-en
promptExpansionModel: Cohee/fooocus_expansion-onnx promptExpansionModel: Cohee/fooocus_expansion-onnx
# -- OPENAI CONFIGURATION -- # -- OPENAI CONFIGURATION --
openai: openai:

View File

@ -0,0 +1,11 @@
{
"story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Alpaca-Single-Turn"
}

View File

@ -0,0 +1,17 @@
{
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\nWrite 1 reply only, italicize actions, and avoid quotation marks. Use markdown. Be proactive, creative, and drive the plot and conversation forward. Include dialog as well as narration.",
"input_sequence": "",
"output_sequence": "",
"first_output_sequence": "<START OF ROLEPLAY>",
"last_output_sequence": "\n### Response:",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": "",
"name": "Alpaca-Single-Turn"
}

View File

@ -275,7 +275,7 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
} }
}); });
DOMPurify.addHook("uponSanitizeAttribute", (_, data, config) => { DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => {
if (!config['MESSAGE_SANITIZE']) { if (!config['MESSAGE_SANITIZE']) {
return; return;
} }
@ -287,7 +287,7 @@ DOMPurify.addHook("uponSanitizeAttribute", (_, data, config) => {
return v; return v;
} }
return "custom-" + v; return 'custom-' + v;
}).join(' '); }).join(' ');
} }
break; break;
@ -4150,6 +4150,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
if (messageBias) { if (messageBias) {
message.extra.bias = messageBias; message.extra.bias = messageBias;
message.mes = removeMacros(message.mes);
} }
await populateFileAttachment(message); await populateFileAttachment(message);

View File

@ -1353,7 +1353,7 @@ class PromptManager {
footerDiv.querySelector('.menu_button:last-child').addEventListener('click', this.handleNewPrompt); footerDiv.querySelector('.menu_button:last-child').addEventListener('click', this.handleNewPrompt);
// Add prompt export dialogue and options // Add prompt export dialogue and options
const exportForCharacter =` const exportForCharacter = `
<div class="row"> <div class="row">
<a class="export-promptmanager-prompts-character list-group-item" data-i18n="Export for character">Export for character</a> <a class="export-promptmanager-prompts-character list-group-item" data-i18n="Export for character">Export for character</a>
<span class="tooltip fa-solid fa-info-circle" title="Export prompts for this character, including their order."></span> <span class="tooltip fa-solid fa-info-circle" title="Export prompts for this character, including their order."></span>

View File

@ -18,6 +18,8 @@ const defaultUrl = 'http://localhost:5100';
let saveMetadataTimeout = null; let saveMetadataTimeout = null;
let requiresReload = false;
export function saveMetadataDebounced() { export function saveMetadataDebounced() {
const context = getContext(); const context = getContext();
const groupId = context.groupId; const groupId = context.groupId;
@ -193,24 +195,32 @@ async function discoverExtensions() {
function onDisableExtensionClick() { function onDisableExtensionClick() {
const name = $(this).data('name'); const name = $(this).data('name');
disableExtension(name); disableExtension(name, false);
} }
function onEnableExtensionClick() { function onEnableExtensionClick() {
const name = $(this).data('name'); const name = $(this).data('name');
enableExtension(name); enableExtension(name, false);
} }
async function enableExtension(name) { async function enableExtension(name, reload = true) {
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name); extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
await saveSettings(); await saveSettings();
location.reload(); if (reload) {
location.reload();
} else {
requiresReload = true;
}
} }
async function disableExtension(name) { async function disableExtension(name, reload = true) {
extension_settings.disabledExtensions.push(name); extension_settings.disabledExtensions.push(name);
await saveSettings(); await saveSettings();
location.reload(); if (reload) {
location.reload();
} else {
requiresReload = true;
}
} }
async function getManifests(names) { async function getManifests(names) {
@ -560,6 +570,7 @@ function getModuleInformation() {
* Generates the HTML strings for all extensions and displays them in a popup. * Generates the HTML strings for all extensions and displays them in a popup.
*/ */
async function showExtensionsDetails() { async function showExtensionsDetails() {
let popupPromise;
try { try {
showLoader(); showLoader();
let htmlDefault = '<h3>Built-in Extensions:</h3>'; let htmlDefault = '<h3>Built-in Extensions:</h3>';
@ -590,13 +601,20 @@ async function showExtensionsDetails() {
${htmlDefault} ${htmlDefault}
${htmlExternal} ${htmlExternal}
`; `;
callPopup(`<div class="extensions_info">${html}</div>`, 'text'); popupPromise = callPopup(`<div class="extensions_info">${html}</div>`, 'text');
} catch (error) { } catch (error) {
toastr.error('Error loading extensions. See browser console for details.'); toastr.error('Error loading extensions. See browser console for details.');
console.error(error); console.error(error);
} finally { } finally {
hideLoader(); hideLoader();
} }
if (popupPromise) {
await popupPromise;
}
if (requiresReload) {
showLoader();
location.reload();
}
} }

View File

@ -49,6 +49,7 @@ let lastMessage = null;
let spriteCache = {}; let spriteCache = {};
let inApiCall = false; let inApiCall = false;
let lastServerResponseTime = 0; let lastServerResponseTime = 0;
export let lastExpression = {};
function isVisualNovelMode() { function isVisualNovelMode() {
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId); return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
@ -692,6 +693,7 @@ function getFolderNameByMessage(message) {
} }
async function sendExpressionCall(name, expression, force, vnMode) { async function sendExpressionCall(name, expression, force, vnMode) {
lastExpression[name.split('/')[0]] = expression;
if (!vnMode) { if (!vnMode) {
vnMode = isVisualNovelMode(); vnMode = isVisualNovelMode();
} }
@ -1476,6 +1478,7 @@ function setExpressionOverrideHtml(forceClear = false) {
// character changed // character changed
removeExpression(); removeExpression();
spriteCache = {}; spriteCache = {};
lastExpression = {};
//clear expression //clear expression
let imgElement = document.getElementById('expression-image'); let imgElement = document.getElementById('expression-image');
@ -1501,4 +1504,5 @@ function setExpressionOverrideHtml(forceClear = false) {
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> force sets the sprite for the current character', true, true); registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> force sets the sprite for the current character', true, true);
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true); registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> Returns the last set sprite / expression for the named character.', true, true);
})(); })();

View File

@ -801,7 +801,7 @@ async function qrDeleteCallback(args, label) {
if (idx === -1) { if (idx === -1) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR with label '${label}' not found`); toastr.warning('Confirm you are using proper case sensitivity!', `QR with label '${label}' not found`);
return ''; return '';
}; }
preset.quickReplySlots.splice(idx, 1); preset.quickReplySlots.splice(idx, 1);
preset.numberOfSlots--; preset.numberOfSlots--;
await fetch('/savequickreply', { await fetch('/savequickreply', {

View File

@ -2642,7 +2642,7 @@ $('#sd_dropdown [id]').on('click', function () {
jQuery(async () => { jQuery(async () => {
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true); registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>') registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>');
$('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings)); $('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
$('#sd_source').on('change', onSourceChange); $('#sd_source').on('change', onSourceChange);

View File

@ -310,12 +310,12 @@ class CoquiTtsProvider {
modelDict = coquiApiModelsFull; modelDict = coquiApiModelsFull;
if (model_setting_language == null & 'languages' in modelDict[model_language][model_dataset][model_label]) { if (model_setting_language == null & 'languages' in modelDict[model_language][model_dataset][model_label]) {
toastr.error('Model language not selected, please select one.', DEBUG_PREFIX+' voice mapping model language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); toastr.error('Model language not selected, please select one.', DEBUG_PREFIX + ' voice mapping model language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return; return;
} }
if (model_setting_speaker == null & 'speakers' in modelDict[model_language][model_dataset][model_label]) { if (model_setting_speaker == null & 'speakers' in modelDict[model_language][model_dataset][model_label]) {
toastr.error('Model speaker not selected, please select one.', DEBUG_PREFIX+' voice mapping model speaker', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); toastr.error('Model speaker not selected, please select one.', DEBUG_PREFIX + ' voice mapping model speaker', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return; return;
} }

View File

@ -1,6 +1,6 @@
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js'; import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from '../../extensions.js'; import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from '../../extensions.js';
import { collapseNewlines, power_user, ui_mode } from '../../power-user.js'; import { collapseNewlines } from '../../power-user.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js'; import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
@ -21,6 +21,7 @@ const settings = {
protect: 5, protect: 5,
insert: 3, insert: 3,
query: 2, query: 2,
message_chunk_size: 400,
// For files // For files
enabled_files: false, enabled_files: false,
@ -87,6 +88,29 @@ async function onVectorizeAllClick() {
let syncBlocked = false; let syncBlocked = false;
/**
* Splits messages into chunks before inserting them into the vector index.
* @param {object[]} items Array of vector items
* @returns {object[]} Array of vector items (possibly chunked)
*/
function splitByChunks(items) {
if (settings.message_chunk_size <= 0) {
return items;
}
const chunkedItems = [];
for (const item of items) {
const chunks = splitRecursive(item.text, settings.message_chunk_size);
for (const chunk of chunks) {
const chunkedItem = { ...item, text: chunk };
chunkedItems.push(chunkedItem);
}
}
return chunkedItems;
}
async function synchronizeChat(batchSize = 5) { async function synchronizeChat(batchSize = 5) {
if (!settings.enabled_chats) { if (!settings.enabled_chats) {
return -1; return -1;
@ -116,8 +140,9 @@ async function synchronizeChat(batchSize = 5) {
const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x)); const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));
if (newVectorItems.length > 0) { if (newVectorItems.length > 0) {
const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize));
console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`); console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`);
await insertVectorItems(chatId, newVectorItems.slice(0, batchSize)); await insertVectorItems(chatId, chunkedBatch);
} }
if (deletedHashes.length > 0) { if (deletedHashes.length > 0) {
@ -492,6 +517,43 @@ function toggleSettings() {
$('#vectors_chats_settings').toggle(!!settings.enabled_chats); $('#vectors_chats_settings').toggle(!!settings.enabled_chats);
} }
async function onPurgeClick() {
const chatId = getCurrentChatId();
if (!chatId) {
toastr.info('No chat selected', 'Purge aborted');
return;
}
await purgeVectorIndex(chatId);
toastr.success('Vector index purged', 'Purge successful');
}
async function onViewStatsClick() {
const chatId = getCurrentChatId();
if (!chatId) {
toastr.info('No chat selected');
return;
}
const hashesInCollection = await getSavedHashes(chatId);
const totalHashes = hashesInCollection.length;
const uniqueHashes = hashesInCollection.filter(onlyUnique).length;
toastr.info(`Total hashes: <b>${totalHashes}</b><br>
Unique hashes: <b>${uniqueHashes}</b><br><br>
I'll mark collected messages with a green circle.`,
`Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false });
const chat = getContext().chat;
for (const message of chat) {
if (hashesInCollection.includes(getStringHash(message.mes))) {
const messageElement = $(`.mes[mesid="${chat.indexOf(message)}"]`);
messageElement.addClass('vectorized');
}
}
}
jQuery(async () => { jQuery(async () => {
if (!extension_settings.vectors) { if (!extension_settings.vectors) {
extension_settings.vectors = settings; extension_settings.vectors = settings;
@ -554,9 +616,9 @@ jQuery(async () => {
Object.assign(extension_settings.vectors, settings); Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#vectors_advanced_settings').toggleClass('displayNone', power_user.ui_mode === ui_mode.SIMPLE);
$('#vectors_vectorize_all').on('click', onVectorizeAllClick); $('#vectors_vectorize_all').on('click', onVectorizeAllClick);
$('#vectors_purge').on('click', onPurgeClick);
$('#vectors_view_stats').on('click', onViewStatsClick);
$('#vectors_size_threshold').val(settings.size_threshold).on('input', () => { $('#vectors_size_threshold').val(settings.size_threshold).on('input', () => {
settings.size_threshold = Number($('#vectors_size_threshold').val()); settings.size_threshold = Number($('#vectors_size_threshold').val());
@ -582,6 +644,12 @@ jQuery(async () => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {
settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
toggleSettings(); toggleSettings();
eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent); eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);

View File

@ -5,7 +5,7 @@
"optional": [], "optional": [],
"generate_interceptor": "vectors_rearrangeChat", "generate_interceptor": "vectors_rearrangeChat",
"js": "index.js", "js": "index.js",
"css": "", "css": "style.css",
"author": "Cohee#1207", "author": "Cohee#1207",
"version": "1.0.0", "version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern" "homePage": "https://github.com/SillyTavern/SillyTavern"

View File

@ -75,7 +75,7 @@
</label> </label>
<div id="vectors_chats_settings"> <div id="vectors_chats_settings">
<div id="vectors_advanced_settings" data-newbie-hidden> <div id="vectors_advanced_settings">
<label for="vectors_template"> <label for="vectors_template">
Insertion Template Insertion Template
</label> </label>
@ -97,17 +97,23 @@
</label> </label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div class="flex1" title="Can increase the retrieval quality for the cost of processing. 0 = disabled.">
<label for="vectors_message_chunk_size">
<small>Chunk size (chars)</small>
</label>
<input id="vectors_message_chunk_size" type="number" class="text_pole widthUnset" min="0" max="9999" />
</div>
<div class="flex1" title="Prevents last N messages from being placed out of order."> <div class="flex1" title="Prevents last N messages from being placed out of order.">
<label for="vectors_protect"> <label for="vectors_protect">
<small>Retain#</small> <small>Retain#</small>
</label> </label>
<input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="99" /> <input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="9999" />
</div> </div>
<div class="flex1" title="How many past messages to insert as memories."> <div class="flex1" title="How many past messages to insert as memories.">
<label for="vectors_insert"> <label for="vectors_insert">
<small>Insert#</small> <small>Insert#</small>
</label> </label>
<input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="99" /> <input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="9999" />
</div> </div>
</div> </div>
</div> </div>
@ -115,8 +121,16 @@
Old messages are vectorized gradually as you chat. Old messages are vectorized gradually as you chat.
To process all previous messages, click the button below. To process all previous messages, click the button below.
</small> </small>
<div id="vectors_vectorize_all" class="menu_button menu_button_icon"> <div class="flex-container">
Vectorize All <div id="vectors_vectorize_all" class="menu_button menu_button_icon">
Vectorize All
</div>
<div id="vectors_purge" class="menu_button menu_button_icon">
Purge Vectors
</div>
<div id="vectors_view_stats" class="menu_button menu_button_icon">
View Stats
</div>
</div> </div>
<div id="vectorize_progress" style="display: none;"> <div id="vectorize_progress" style="display: none;">
<small> <small>

View File

@ -0,0 +1,4 @@
.mes.vectorized .name_text::after {
content: '🟢';
margin-left: 5px;
}