Compare commits

...

123 Commits

Author SHA1 Message Date
Cohee
6f5dbc2a52 Merge pull request #1866 from SillyTavern/staging
Staging
2024-02-27 21:11:16 +02:00
Cohee
0fcb176408 Bump package version 2024-02-27 21:10:19 +02:00
Cohee
31f39e30c2 Merge pull request #1864 from Fyphen1223/release
Update Japanese translation
2024-02-27 20:31:26 +02:00
Cohee
5a236fbccb Merge pull request #1865 from deciare/edit-message-macros
Parse macros when updating message
2024-02-27 20:25:17 +02:00
Deciare
2a4b8ac438 Update displayed prompt bias when auto-saving edits.
When Auto-save Message Edits is enabled, the prompt bias string
displayed beneath the textarea wasn't being updated.
2024-02-27 05:17:38 -05:00
Deciare
7885f19e86 Perform macro substitution while updating message.
This addresses 3 issues:
1. Prompt bias string was not removed from the text of the edited
  message.
2. Macro substitition was not performed in the prompt bias string.
3. Macro substitution was not performed in the edited message text.
2024-02-27 05:17:31 -05:00
Fyphen
1898192d37 Update readme-ja_jp.md 2024-02-27 18:00:56 +09:00
Cohee
29c4334c46 #1859 Set keep_alive for ollama 2024-02-26 21:09:21 +02:00
Cohee
73886c9fff Merge pull request #1863 from kingbased/mistral
mistral-large
2024-02-26 20:10:51 +02:00
based
149a65cf62 migrate model name in old presets to new naming scheme 2024-02-27 02:23:07 +10:00
based
617ae7d02c ??? 2024-02-27 01:42:22 +10:00
based
c58d0b2b94 subvers 2024-02-27 01:12:17 +10:00
based
e86fd08d0f update mistral models 2024-02-27 01:02:02 +10:00
Cohee
f962ad5c02 Add OpenRouter as a text completion source 2024-02-25 22:47:07 +02:00
Cohee
9e5505a7d4 Autocomplete for WI automation IDs 2024-02-25 03:54:40 +02:00
Cohee
fc289126fa Add event type for text completion generation request settings ready 2024-02-24 21:45:33 +02:00
Cohee
d5bf9fc28c Non-streaming logprobs for Aphrodite 2024-02-24 20:53:23 +02:00
Cohee
d140b8d5be Parse non-streaming tabby logprobs 2024-02-24 20:10:53 +02:00
Cohee
3cedf64f66 Add autocomplete for WI inclusion groups 2024-02-24 19:04:44 +02:00
Cohee
0e357c191b Align label margins 2024-02-24 18:23:58 +02:00
Cohee
3441667336 #1853 Add WI/Script link by entry automation id 2024-02-24 17:22:51 +02:00
Cohee
7b8ac8f4c4 Properly use vector insert setting 2024-02-24 15:57:26 +02:00
Cohee
16833fc238 Merge pull request #1855 from deciare/llamacpp-sampler-order
Sampler order for llama.cpp server backend
2024-02-24 15:45:44 +02:00
Cohee
8848818d67 Fix dynatemp neutralization 2024-02-24 15:32:12 +02:00
Cohee
299bd9d563 Merge branch 'staging' into llamacpp-sampler-order 2024-02-24 15:10:58 +02:00
Cohee
13aebc623a Merge pull request #1854 from deciare/llamacpp-probs
Request and display token probabilities from llama.cpp backend
2024-02-24 15:06:28 +02:00
Cohee
eaadfea639 Extend debounce duration of logprobs renderer 2024-02-24 15:03:57 +02:00
Cohee
9287ff18de Fix for non-streaming 2024-02-24 14:50:06 +02:00
Cohee
dab9bbb514 Merge pull request #1844 from infermaticAI/InfermaticAI
Add InfermaticAI as a text completion source
2024-02-24 14:28:09 +02:00
Deciare
445cbda02f If token probability is a logarithm it'll be < 0
No need to read settings to find out if llama.cpp backend is in use...
2024-02-24 00:13:33 -05:00
Deciare
9eba076ae4 Sampler order for llama.cpp server backend 2024-02-23 23:01:04 -05:00
Deciare
936fbac6c5 Merge remote-tracking branch 'origin/staging' into llamacpp-probs 2024-02-23 17:45:54 -05:00
Cohee
737a0bd3ae Fix purge extras and mistral vectors 2024-02-23 22:37:00 +02:00
Cohee
9b34ac1bde Merge pull request #1852 from berbant/staging
Display TranslateProvider link
2024-02-23 21:43:59 +02:00
Cohee
cb536a7611 Save a list of safe to export secret keys 2024-02-23 21:41:54 +02:00
Cohee
82c5042bad Prevent extra loop iterations on buffer init 2024-02-23 21:23:44 +02:00
Cohee
4baefeba68 Extend per-entry scan depth limit, add warnings on overflow 2024-02-23 21:18:40 +02:00
Deciare
344b9eedbc Request token probabilities from llama.cpp backend
llama.cpp server token probabilities are given as values ranging from
0 to 1 instead of as logarithms.
2024-02-23 14:01:46 -05:00
NWilson
f82740a238 Change Non-streaming Handler 2024-02-22 15:51:11 -06:00
berbant
bc2010a762 Update secrets.js 2024-02-22 23:55:57 +04:00
berbant
eb89337f51 Update index.js 2024-02-22 23:49:47 +04:00
Cohee
c9f0d61f19 #1851 Substitute macros in new example chat 2024-02-22 18:45:50 +02:00
NWilson
f569424f3e Merge branch 'staging' into InfermaticAI 2024-02-22 08:32:10 -06:00
Cohee
beb5e470a2 #1069 Fix hoisting of pristine cards in newest sort 2024-02-22 04:48:46 +02:00
Cohee
ece3b2a7c1 Fix Chat Completions status check on settings loading if another API is selected 2024-02-22 04:36:06 +02:00
Cohee
06c3ea7c51 Merge pull request #1811 from kalomaze/sampler-order-ooba
Sampler priority support (for text-generation-webui)
2024-02-22 02:55:38 +02:00
Cohee
0ccdfe4bb7 Fix duped line 2024-02-22 02:45:35 +02:00
Cohee
40aa971d11 Merge branch 'staging' into sampler-order-ooba 2024-02-22 02:44:32 +02:00
Cohee
fb6fa54c7f Fix import fetch HTTP method 2024-02-21 19:57:38 +02:00
Cohee
fcf171931a Merge pull request #1846 from SillyTavern/pygimport
Pygimport
2024-02-21 19:55:57 +02:00
Cohee
92af4137a9 Use new export endpoint 2024-02-21 11:28:59 +02:00
Cohee
711fd0517f Merge branch 'staging' into pygimport 2024-02-21 11:26:47 +02:00
Cohee
d31195a704 Apply same width for Kobold order
Just in case
2024-02-21 01:02:23 +02:00
Cohee
10fb69f36a Widen the block 2024-02-21 00:59:38 +02:00
Cohee
d353fa58d0 Close div properly 2024-02-21 00:56:40 +02:00
Cohee
96f1ce1fce Skill issue? 2024-02-21 00:55:30 +02:00
Cohee
0c1cf9ff2e Send sampler priority as array 2024-02-21 00:53:54 +02:00
NWilson
7c12c836f2 Implement Key Filter 2024-02-20 09:40:35 -06:00
NWilson
48b9eb8542 Revert "Add InfermaticAI Profile"
This reverts commit 1e7c2820da.
2024-02-20 09:37:39 -06:00
Cohee
f43e686301 Merge pull request #1845 from underscorex86/patch-1
Update slash-commands.js
2024-02-20 16:57:25 +02:00
Cohee
f0141b4dd1 Update slash-commands.js 2024-02-20 16:57:00 +02:00
NWilson
1e7c2820da Add InfermaticAI Profile 2024-02-20 08:12:59 -06:00
Sneha C
095cd873de Update slash-commands.js
added the word "persona" to the /sync description to make it easier for users to find.
2024-02-20 16:48:43 +04:00
Cohee
8ba9b5c38b Merge branch 'staging' into sampler-order-ooba 2024-02-20 02:32:33 +02:00
Cohee
8e66a14e37 Add hints to doc strings about additional command prompts 2024-02-20 02:29:14 +02:00
Cohee
79ba026486 Merge pull request #1840 from Wolfsblvt/slash-commands-menu-actions-allow-custom-prompts
Extend impersonate/continue/regenerate with possible custom prompts (via slash commands and popup menu)
2024-02-20 02:26:41 +02:00
kalomaze
cec0698400 Oopsie 2024-02-19 18:24:04 -06:00
kalomaze
f3971686ea Move text-gen-webui sampler order under kcpp order 2024-02-19 18:18:57 -06:00
kalomaze
32ee58e5e6 fix kcpp order reset 2024-02-19 18:12:56 -06:00
kalomaze
0d8858285f Merge branch 'SillyTavern:release' into sampler-order-ooba 2024-02-19 18:11:30 -06:00
Cohee
061b7c6922 Don't try to execute script commands if the message doesn't start with slash 2024-02-20 02:09:01 +02:00
Wolfsblvt
a5ee46cb2a Only respect slash command, ignore text field 2024-02-19 22:36:32 +01:00
Wolfsblvt
550d8483cc Extend impersonate/continue/regenerate with possible custom prompts
- Use custom prompt provided via slash command arguments (similar to /sysgen and others)
- Use written text from textbox, if the popout menu actions are clicked
2024-02-19 22:23:58 +01:00
Cohee
2e00a1baaf [FEATURE_REQUEST] Can the unlocked max context size for OpenAI completion be increased from 102k to 200k for example? #1842 2024-02-19 19:37:18 +02:00
NWilson
030806bf1e Merge remote-tracking branch 'origin/staging' into InfermaticAI 2024-02-19 10:14:06 -06:00
NWilson
e55d903613 Support more settings 2024-02-19 09:53:26 -06:00
NWilson
90d5fbc182 Fix non streaming 2024-02-19 09:46:56 -06:00
Cohee
3c2113a6e7 Add ability to preserve file names when loading from assets downloader 2024-02-19 00:17:23 +02:00
Cohee
0391179c3c Remove HTTP/2 workaround for pygsite 2024-02-17 17:04:37 +02:00
Cohee
e4a48cd28f Add pyg hint to import UI 2024-02-17 03:54:13 +02:00
Cohee
c20a9fb5f5 Add HTTP/2 workaround for pygsite import 2024-02-17 03:52:45 +02:00
Cohee
02a2e26e2b Merge branch 'staging' into pygimport 2024-02-17 02:23:18 +02:00
Cohee
ec826450dc Merge pull request #1808 from LenAnderson/slash-fix-bleed
stop named args from nested commands bleeding into parent
2024-02-16 20:55:59 +02:00
Cohee
29b971a986 Merge branch 'staging' into slash-fix-bleed 2024-02-16 20:48:32 +02:00
Cohee
b072057594 [skip ci] Code format 2024-02-16 20:44:54 +02:00
Cohee
c06fe6abfc Add character asset type 2024-02-16 20:42:56 +02:00
Cohee
a8cd6c9fe7 Allow finding characters in slash commands by exact PNG name 2024-02-16 20:24:47 +02:00
Cohee
7fbef32869 Use uuid extraction from Pygsite URL 2024-02-16 20:24:06 +02:00
Cohee
106cdf3aed Merge pull request #1833 from berbant/patch-4
Update chat-completions.js (сhanges to openrouter referer)
2024-02-16 20:22:55 +02:00
NWilson
c6c73fedad Key Fix 2024-02-16 10:23:26 -06:00
Cohee
c8b0030f6e Extract PNG read/write methods 2024-02-16 18:03:56 +02:00
NWilson
8075e4cd1e Changes 2024-02-16 09:07:06 -06:00
NWilson
b5887960b6 Merge branch 'release' into InfermaticAI 2024-02-16 08:53:04 -06:00
berbant
187ecc2046 Update chat-completions.js
This fix will let SillyTavern show up in the Activity List and App Showcase List (app rankings) on openrouter.ai with the right name and the correct link. Right now, all the requests from ST on their end look like 'http://127.0.0.1:'.
2024-02-15 16:14:33 +04:00
Cohee
0da0d490c7 #1796 Attempt to fix alltalk on remote servers 2024-02-14 19:44:47 +02:00
Cohee
6ab918605e Allow single quotes in generated SD prompts 2024-02-14 15:39:56 +02:00
Cohee
e3102bb26f #1823 Add -1 as min value for top k 2024-02-14 01:45:07 +02:00
Cohee
dbfe7ae7c6 Remove trailing whitespace 2024-02-13 11:56:57 +02:00
Cohee
f12993ffb7 Merge pull request #1824 from SolidSnacke/release
Translating to Russian language. (third addition)
2024-02-13 11:56:41 +02:00
Cohee
8d4c4c1945 Only snap resolution if pixel counts change 2024-02-13 11:40:37 +02:00
SolidSnacke
604f9732be Small fix. 2024-02-12 19:42:07 +03:00
SolidSnacke
90963f6437 Addendum number four.
I reached for everything that most often caught my eye.
I also replaced widthNatural flex1 margin0 with widthNatural margin0. Now there should be no problem with fields taking up extra space. Although perhaps you asked me to return everything as it was.
2024-02-12 18:58:52 +03:00
Cohee
b8387df15e #1826 Allow snapping to known resolutions for face/bg generations 2024-02-12 17:28:39 +02:00
Cohee
9d713825c2 #1827 Consolidate {{group}} macro behavior 2024-02-12 16:23:01 +02:00
Cohee
5f6bc49aa6 Exclude repo from droid media scans 2024-02-12 15:56:14 +02:00
Cohee
72256110a7 Unbreak current chat rename 2024-02-12 02:55:16 +02:00
SolidSnacke
85df989193 Translating to Russian language. (third addition)
I finished the translation into Russian a little more as best I could.
2024-02-12 02:28:42 +03:00
Cohee
0480488127 Merge pull request #1815 from bdashore3/staging
Fix zen slider values
2024-02-11 19:27:37 +02:00
Cohee
fe080cfec3 Merge pull request #1817 from SolidSnacke/release
Translating to Russian language. Translation of the User Settings tab …. (again)
2024-02-11 19:27:18 +02:00
Cohee
7bcb6f1ee1 Merge branch 'release' into staging 2024-02-11 19:22:20 +02:00
SolidSnacke
4d958b9df7 Translating to Russian language. Translation of the User Settings tab. (again)
Made a translation of the User Settings tab. I tried to translate everything that was not translated and tried to correct the index.html file in some places so that, for example, the PC and phone icons would not disappear during translation. Next, I'll try to translate other tabs if I'm not too lazy.
By the way, I wanted to ask, how can I translate the pop-up message in these buttons? I just asked once and they wrote to me that they are a different module altogether. (see photo)
2024-02-11 20:06:47 +03:00
Cohee
354c52d997 #1814 Fix regex placement attribution 2024-02-11 16:52:14 +02:00
Cohee
33d93b9761 #1813 Fix squash system messages 2024-02-11 15:56:48 +02:00
kingbri
6e10c43c63 Zen Sliders: Add off values for dynatemp and smoothing factor
Off for dynatemp is 1.0, smoothing factor is 0.0

Signed-off-by: kingbri <bdashore3@proton.me>
2024-02-11 01:32:32 -05:00
kalomaze
9fed7ed742 Make the neutralize option turn off quad sampling 2024-02-10 17:57:00 -06:00
kalomaze
70deb11d27 implement jank js + drag n drop sampler priority 2024-02-10 16:32:46 -06:00
kalomaze
818029288e Remove sending it if it's Aphrodite or TabbyAPI 2024-02-10 15:22:24 -06:00
Cohee
cbea5bf996 UI Theme import/export 2024-02-10 23:12:16 +02:00
kalomaze
2065f95edc Sampler priority support 2024-02-10 14:57:41 -06:00
Cohee
87668f5962 Update tiktoken package 2024-02-10 22:16:59 +02:00
Cohee
df97f5364b Regex import/export 2024-02-10 21:57:22 +02:00
LenAnderson
cab6f90519 stop named args from nested commands bleeding into parent 2024-02-09 22:49:38 +00:00
NWilson
f29f934c6b Progress 2024-01-24 06:59:27 -06:00
47 changed files with 2046 additions and 539 deletions

View File

@@ -2,15 +2,15 @@
![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、自動翻訳、あなたにとって必要とする以上のプロンプトオプション+サードパーティの拡張機能をインストールする機能
モバイルデバイスにも適したレイアウトマルチ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 のフォークに基づいています
[TavernAI](https://github.com/TavernAI/TavernAI) v.1.2.8 のフォークに基づいています
## 重要ニュース!
## 重要事項
1. 私たちは[ドキュメント website](https://docs.sillytavern.app/) を作成し、ほとんどの質問にお答えしています。
1. 私たちは[ドキュメントウェブサイト](https://docs.sillytavern.app/) を作成し、ほとんどの質問にお答えしています。
2. アップデートしたら拡張機能を見失った?リリースバージョン 1.10.6 以降、これまで内蔵されていた拡張機能のほとんどがダウンロード可能なアドオンに変更されました。ダウンロードは、拡張機能パネル(トップバーのスタックドブロックアイコン)にある内蔵の "Download Extensions and Assets" メニューから行えます。
2. アップデートしたら使っていた拡張機能を見失いましたか?リリースバージョン 1.10.6 以降、これまで内蔵されていた拡張機能のほとんどがダウンロード可能なアドオンに変更されました。ダウンロードは、拡張機能パネル(トップバーのスタックドブロックアイコン)にある内蔵の "Download Extensions and Assets" メニューから行えます。
### Cohee、RossAscends、SillyTavern コミュニティがお届けします
@@ -27,21 +27,21 @@ SillyTavern は、すべてのユーザーにスムーズな体験を保証す
* release -🌟 **ほとんどのユーザーにお勧め。** これは最も安定した推奨ブランチで、メジャーリリースがプッシュされた時のみ更新されます。大半のユーザーに適しています。
* staging - ⚠️ **カジュアルな使用にはお勧めしない。** このブランチには最新の機能がありますが、いつ壊れるかわからないので注意してください。パワーユーザーとマニア向けです。
git CLI の使い方に慣れていなかったり、ブランチが何なのかわからなかったりしても、心配はいりません!リリースブランチが常に望ましい選択肢となります。
git CLI の使い方に慣れていなかったり、ブランチが何なのかわからなかったりしても、心配はいりません!releaseブランチが良い選択肢となります。
### Tavern 以外に何が必要ですか?
Tavern は単なるユーザーインターフェイスなので、それだけでは役に立ちません。ロールプレイキャラクターとして機能する AI システムのバックエンドにアクセスする必要があります。様々なバックエンドがサポートされています: OpenAPI API (GPT)、KoboldAI (ローカルまたは Google Colab 上で動作)、その他。詳しくは [FAQ](https://docs.sillytavern.app/usage/faq/) をご覧ください。
Tavern は単なるユーザーインターフェイスなので、それだけでは何もできません。ロールプレイキャラクターとして機能する AI システムのバックエンドにアクセスする必要があります。様々なバックエンドがサポートされています。リストはこちらです OpenAPI API (GPT)、KoboldAI (ローカルまたは Google Colab 上で動作)、その他。詳しくは [FAQ](https://docs.sillytavern.app/usage/faq/) をご覧ください。
### Tavern を実行するには、強力な PC が必要ですか?
### Tavern を実行するには、ハイスペックなPCが必要ですか
Tavern は単なるユーザーインターフェイスであり、必要なハードウェアはごくわずかです。パワフルである必要があるのは、AI システムのバックエンドです。
Tavern は単なるフロントエンドのUIであり、必要なハードウェアはごくわずかです。パワフルである必要があるのは、AI システムのバックエンドです。
## モバイルサポート
> **注**
> **このフォークは Termux を使って Android スマホでネイティブに実行できます。ArroganceComplex#2659 のガイドを参照してください:**
> **このフォークは Termux を使って Android スマホで実行できます。ArroganceComplex#2659 のガイドを参照してください:**
<https://rentry.org/STAI-Termux>
@@ -49,19 +49,19 @@ Tavern は単なるユーザーインターフェイスであり、必要なハ
### コミュニティ 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% 以上が書き換えまたは最適化されています)
* スワイプ
@@ -76,7 +76,7 @@ Tavern は単なるユーザーインターフェイスであり、必要なハ
## 拡張機能
SillyTavern は拡張性をサポートしており、[SillyTavern Extras API](https://github.com/SillyTavern/SillyTavern-extras) を介していくつかの追加AIモジュールをホストしています
SillyTavern は拡張できるようになっており、[SillyTavern Extras API](https://github.com/SillyTavern/SillyTavern-extras) いくつかの追加AIモジュールをホストしています
* 作者ノート/キャラクターバイアス
* キャラクターの感情表現(スプライト)
@@ -97,7 +97,7 @@ SillyTavern は拡張性をサポートしており、[SillyTavern Extras API](h
* Right = 右スワイプ (注: チャットバーに何か入力されている場合、スワイプホットキーが無効になります)
* Ctrl+Left = ローカルに保存された変数を見る(ブラウザのコンソールウィンドウにて)
* Enter (チャットバー選択時) = AI にメッセージを送る
* Ctrl+Enter = 最後の AI 応答を再生成する
* Ctrl+Enter = 最後の AIのレスポンスを再生成する
* ユーザー名の変更と文字の削除でページが更新されなくなりました。
@@ -136,10 +136,10 @@ SillyTavern は拡張性をサポートしており、[SillyTavern Extras API](h
Git 経由でのインストール(更新を容易にするため推奨)
きれいな写真付きのわかりやすいガイド:
写真付きのわかりやすいガイドはこちらです:
<https://docs.sillytavern.app/installation/windows/>
1. [NodeJS](https://nodejs.org/en) をインストールする(最新の LTS 版を推奨)
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\
@@ -158,7 +158,7 @@ ZIP ダウンロードによるインストール(推奨しない)
2. GitHub のリポジトリから zip をダウンロードする。(`ソースコード(zip)` は [Releases](https://github.com/SillyTavern/SillyTavern/releases/latest) から入手)
3. お好きなフォルダに解凍してください
4. `Start.bat` をダブルクリックまたはコマンドラインで実行する。
5. サーバーがあなたのためにすべてを準備したら、ブラウザのタブを開きます。
5. サーバーが準備できれば、ブラウザのタブを開きます。
### Linux
@@ -211,7 +211,7 @@ CIDR マスクも受け付ける10.0.0.0/24
* `whitelist.txt` ファイルを保存する。
* TAI サーバーを再起動する。
これでファイルに指定された IP を持つデバイスが接続できるようにな
これでファイルに指定された IP を持つデバイスが接続できるようになります
*注: `config.yaml` にも `whitelist` 配列があり、同じように使うことができるが、`whitelist.txt` が存在する場合、この配列は無視される。*
@@ -258,7 +258,7 @@ ST サーバを再起動すると、ユーザ名とパスワードさえ知っ
ユーザー設定パネルでブラー効果なし(高速 UIモードを有効にしてみてください。
## このプロジェクトが好きです!どうすればコントリビュートできますか?
## このプロジェクトが気に入りました!どうすれば貢献できますか?
### やるべきこと
@@ -266,7 +266,7 @@ ST サーバを再起動すると、ユーザ名とパスワードさえ知っ
2. 確立されたテンプレートを使って機能提案と課題レポートを送る
3. 何か質問する前に、readme ファイルや組み込みのドキュメントを読んでください
### やらないべきこと
### やるべきではないこと
1. 金銭の寄付を申し出る
2. 何の脈絡もなくバグ報告を送る

0
.nomedia Normal file
View File

View File

@@ -47,6 +47,28 @@
"ban_eos_token": false,
"skip_special_tokens": true,
"streaming": false,
"sampler_priority": [
"temperature",
"dynamic_temperature",
"quadratic_sampling",
"top_k",
"top_p",
"typical_p",
"epsilon_cutoff",
"eta_cutoff",
"tfs",
"top_a",
"min_p",
"mirostat"
],
"samplers": [
"top_k",
"tfs_z",
"typical_p",
"top_p",
"min_p",
"temperature"
],
"mirostat_mode": 0,
"mirostat_tau": 5,
"mirostat_eta": 0.1,

12
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "sillytavern",
"version": "1.11.4",
"version": "1.11.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.11.4",
"version": "1.11.5",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.2",
"@dqbd/tiktoken": "^1.0.13",
"bing-translate-api": "^2.9.1",
"body-parser": "^1.20.2",
"command-exists": "^1.2.9",
@@ -78,9 +78,9 @@
"integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA=="
},
"node_modules/@dqbd/tiktoken": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.7.tgz",
"integrity": "sha512-bhR5k5W+8GLzysjk8zTMVygQZsgvf7W1F0IlL4ZQ5ugjo5rCyiwGM5d8DYriXspytfu98tv59niang3/T+FoDw=="
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.13.tgz",
"integrity": "sha512-941kjlHjfI97l6NuH/AwuXV4mHuVnRooDcHNSlzi98hz+4ug3wT4gJcWjSwSZHqeGAEn90lC9sFD+8a9d5Jvxg=="
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",

View File

@@ -2,7 +2,7 @@
"dependencies": {
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.2",
"@dqbd/tiktoken": "^1.0.13",
"bing-translate-api": "^2.9.1",
"body-parser": "^1.20.2",
"command-exists": "^1.2.9",
@@ -55,7 +55,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.11.4",
"version": "1.11.5",
"scripts": {
"start": "node server.js",
"start-multi": "node server.js --disableCsrf",

View File

@@ -1668,7 +1668,7 @@
"Delete persona": "주인공 삭제하기"
},
"ru-ru": {
"clickslidertips": "Можно установить вручную, использовав цифру рядом с ползунком",
"clickslidertips": "Щелкните на цифру ползунка, чтобы вписать вручную.",
"kobldpresets": "Предустановки Kobold",
"guikoboldaisettings": "Интерфейс KoboldAI",
"novelaipreserts": "Предустановки NovelAI",
@@ -1680,11 +1680,45 @@
"context size(tokens)": "Размер контекста (в токенах)",
"unlocked": "Неограниченный",
"Only select models support context sizes greater than 4096 tokens. Increase only if you know what you're doing.": "Только отдельные модели поддерживают контекст, превышающий 4096 токенов. Используйте только если понимаете, что делаете.",
"rep.pen": "Rep. Pen.",
"rep.pen range": "Диапазон Rep. Pen.",
"temperature": "Температура",
"Encoder Rep. Pen.": "Расшифровщик Rep. Pen.",
"No Repeat Ngram Size": "No Repeat Ngram Size",
"rep.pen": "Штраф за повторение",
"WI Entry Status:🔵 Constant🟢 Normal❌ Disabled": "Статус входа WI:\n 🔵 Константа\n 🟢 Cтандартный\n ❌ Отключен",
"rep.pen range": "Диапазон штрафов за повтор.",
"Temperature controls the randomness in token selection": "Temperature контролирует случайность выбора токенов:\n- низкая Temperature (<1.0) приводит к более предсказуемому тексту, отдавая предпочтение токенам с высокой вероятностью.\n- высокая Temperature (>1.0) повышает креативность и разнообразие вывода, давая токенам с низкой вероятностью больше шансов.\nУстановите значение 1.0 для исходных вероятностей.",
"temperature": "Temperature",
"Top K sets a maximum amount of top tokens that can be chosen from": "Top K задает максимальное количество токенов, которые могут быть выбраны.\nЕсли Top-K равен 20, это означает, что будут сохранены только 20 токенов с наивысшим рейтингом (независимо от того, что их вероятности разнообразны или ограничены)\nУстановите значение 0, чтобы отключить.",
"Top P (a.k.a. nucleus sampling)": "Top P (также известная как выборка ядра) складывает все верхние токены, необходимые для достижения целевого процента.\nТо есть, если 2 верхних токена составляют 25%, а Top-P равен 0.50, учитываются только эти 2 верхних токена.\nУстановите значение 1.0, чтобы отключить.",
"Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set": "Сэмплер Typical P определяет приоритет токенов на основе их отклонения от средней энтропии набора.\nОстаются токены, чья кумулятивная вероятность близка к заданному порогу (например, 0,5), выделяя те, которые имеют среднее информационное содержание.\nУстановите значение 1.0, чтобы отключить.",
"Min P sets a base minimum probability": "Min P устанавливает базовую минимальную вероятность. Она масштабируется в зависимости от вероятности верхнего токена.\nЕсли вероятность верхнего токена составляет 80%, а Min P - 0.1, будут рассматриваться только токены с вероятностью выше 8%.\nУстановите значение 0, чтобы отключить.",
"Top A sets a threshold for token selection based on the square of the highest token probability": "Top A устанавливает порог для отбора токенов на основе квадрата наибольшей вероятности токена.\nЕсли значение Top A равно 0.2, а вероятность верхнего токена равна 50%, то токены с вероятностью ниже 5% (0.2 * 0.5^2) будут исключены.\nУстановите значение 0, чтобы отключить.",
"Tail-Free Sampling (TFS)": "Tail-Free Sampling (TFS) ищет хвост маловероятных токнов в распределении,\n анализируя скорость изменения вероятностей токенов с помощью производных. Он сохраняет токены до порога (например, 0.3), основанного на нормированной второй производной.\nЧем ближе к 0, тем больше отброшенных токенов. Установите значение 1.0, чтобы отключить.",
"Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "Epsilon cutoff устанавливает уровень вероятности, ниже которого токены исключаются из выборки.\nВ единицах 1e-4; разумное значение - 3.\nУстановите 0, чтобы отключить.",
"Scale Temperature dynamically per token, based on the variation of probabilities": "Динамическое масштабирование Temperature для каждого токена, основанное на изменении вероятностей.",
"Minimum Temp": "Минимальная Temp",
"Maximum Temp": "Максимальная Temp",
"Exponent": "Экспонента",
"Mirostat Mode": "Режим",
"Mirostat Tau": "Tau",
"Mirostat Eta": "Eta",
"Variability parameter for Mirostat outputs": "Параметр изменчивости для выходных данных Mirostat.",
"Learning rate of Mirostat": "Скорость обучения Mirostat.",
"Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "Сила условия регуляризации контрастивного поиска. Установите значение 0, чтобы отключить CS.",
"Temperature Last": "Temperature Last",
"Use the temperature sampler last": "Использовать Temperature сэмплер в последнюю очередь. Это почти всегда разумно.\nПри включении: сначала выборка набора правдоподобных токенов, затем применение Temperature для корректировки их относительных вероятностей (технически, логитов).\nПри отключении: сначала применение Temperature для корректировки относительных вероятностей ВСЕХ токенов, затем выборка правдоподобных токенов из этого.\nОтключение Temperature Last увеличивает вероятности в хвосте распределения, что увеличивает шансы получить несогласованный ответ.",
"LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Убедитесь, что сначала выбрали подходящий токенизатор.\nПоследовательности, которые вы не хотите видеть в выходных данных.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены.",
"Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]",
"Classifier Free Guidance. More helpful tip coming soon": "Руководство без классификатора. Больше полезных советов в ближайшее время.",
"Scale": "Масштаб",
"GBNF Grammar": "Грамматика GBNF",
"Usage Stats": "Статистика исп.",
"Click for stats!": "Нажмите для получения статистики!",
"Backup": "Резер. копирование",
"Backup your personas to a file": "Резервное копирование персон в файл",
"Restore": "Восстановить",
"Restore your personas from a file": "Восстановление персон из файла",
"Type in the desired custom grammar": "Введите нужную пользовательскую грамматику",
"Encoder Rep. Pen.": "Штраф за кодирование",
"Smoothing Factor": "Коэффициент сглаживания",
"No Repeat Ngram Size": "Нет повторов размера Ngram",
"Min Length": "Минимальная длина",
"OpenAI Reverse Proxy": "Прокси с OpenAI",
"Alternative server URL (leave empty to use the default value).": "Альтернативный URL сервера (оставьте пустым для стандартного значения)",
@@ -1694,21 +1728,21 @@
"Enable this if the streaming doesn't work with your proxy": "Включите это, если потоковый вывод текста не работает с вашим прокси",
"Context Size (tokens)": "Размер контекста (в токенах)",
"Max Response Length (tokens)": "Максимальная длина ответа (в токенах)",
"Temperature": "Температура",
"Temperature": "Temperature",
"Frequency Penalty": "Штраф за частоту",
"Presence Penalty": "Штраф за присутствие",
"Top-p": "Top-p",
"Top-p": "Top P",
"Display bot response text chunks as they are generated": "Отображать ответ ИИ по мере генерации текста",
"Top A": "Top-a",
"Typical Sampling": "Типичная выборка",
"Tail Free Sampling": "Бесхвостовая выборка",
"Rep. Pen. Slope": "Rep. Pen. Склон",
"Top A": "Top А",
"Typical Sampling": "Typical Sampling",
"Tail Free Sampling": "Tail Free Sampling",
"Rep. Pen. Slope": "Rep. Pen. Slope",
"Single-line mode": "Режим одной строки",
"Top K": "Top-k",
"Top P": "Top-p",
"Top K": "Top K",
"Top P": "Top P",
"Do Sample": "Сделать образец",
"Add BOS Token": "Добавить BOS-токен",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "Добавлять BOS-токен в начале инструкции. Выключение этого может сделать ответы более креативными. ",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "Добавлять BOS-токен в начале инструкции. Выключение этого может сделать ответы более креативными. ",
"Ban EOS Token": "Заблокировать EOS-токен",
"Ban the eos_token. This forces the model to never end the generation prematurely": "Блокировка EOS-токена вынудит модель никогда не завершать генерацию преждевременно",
"Skip Special Tokens": "Пропускать специальные токены",
@@ -1717,12 +1751,13 @@
"Length Penalty": "Штраф за длину",
"Early Stopping": "Преждевременная остановка",
"Contrastive search": "Контрастный поиск",
"Penalty Alpha": "Штраф Альфа",
"Penalty Alpha": "Penalty Alpha",
"Seed": "Зерно",
"Epsilon Cutoff": "Отсечение эпсилона",
"Eta Cutoff": "Отсечка Eta",
"Epsilon Cutoff": "Epsilon Cutoff",
"Eta Cutoff": "Eta Cutoff",
"Negative Prompt": "Отрицательная подсказка",
"Mirostat (mode=1 is only for llama.cpp)": "Mirostat (режим = 1 только для llama.cpp)",
"Mirostat (mode=1 is only for llama.cpp)": "Mirostat",
"Mirostat is a thermostat for output perplexity": "Mirostat - это термостат для недоумения на выходе.\nMirostat подгоняет недоумение на выходе к недоумению на входе, что позволяет избежать ловушки повторения.\n(когда по мере того, как авторегрессионный вывод производит текст, недоумение на выходе стремится к нулю)\n и ловушки путаницы (когда недоумение расходится)\nДля подробностей смотрите статью Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).\nРежим выбирает версию Mirostat. 0=отключить, 1=Mirostat 1.0 (только llama.cpp), 2=Mirostat 2.0.",
"Add text here that would make the AI generate things you don't want in your outputs.": "Добавьте сюда текст, который заставит ИИ генерировать то, что вы не хотите видеть в своих выводах",
"Phrase Repetition Penalty": "Штраф за повторение фразы",
"Preamble": "Преамбула",
@@ -1739,8 +1774,14 @@
"NSFW Prioritized": "Предпочитать NSFW",
"NSFW prompt text goes first in the prompt to emphasize its effect.": "Отправлять NSFW-инструкцию в начале для усиления его эффекта",
"Streaming": "Потоковый вывод текста",
"Display the response bit by bit as it is generated.": "Отображать ответ по кускам в процессе генерации.",
"When this is off, responses will be displayed all at once when they are complete.": "Если данная функция отключена, ответ будет отображен полностью после генерации.",
"Dynamic Temperature": "Динамическая Temperature",
"Restore current preset": "Восстановить текущую предустановку",
"Neutralize Samplers": "Нейтрализовать сэмплеры",
"Text Completion presets": "Предустановки Text Completion",
"Documentation on sampling parameters": "Документация по параметрам сэмплеров",
"Set all samplers to their neutral/disabled state.": "Установить все сэмплеры в нейтральное/отключенное состояние.",
"Only enable this if your model supports context sizes greater than 4096 tokens": "Включите эту опцию, только если ваша модель поддерживает размер контекста более 4096 токенов.\nУвеличивайте только если вы знаете, что делаете.",
"Display the response bit by bit as it is generated": "Отображение ответа бит за битом по мере его генерации.\nКогда этот параметр выключен, ответы будут отображаться все сразу после их завершения.",
"Generate only one line per request (KoboldAI only, ignored by KoboldCpp).": "Генерируйте только одну строку для каждого запроса (только KoboldAI, игнорируется KoboldCpp).",
"Ban the End-of-Sequence (EOS) token (with KoboldCpp, and possibly also other tokens with KoboldAI).": "Запретите токен конца последовательности (EOS) (с помощью KoboldCpp и, возможно, также других токенов с помощью KoboldAI).",
"Good for story writing, but should not be used for chat and instruct mode.": "Подходит для написания историй, но не должен использоваться в режиме чата и инструктирования.",
@@ -1772,14 +1813,25 @@
"API": "API",
"KoboldAI": "KoboldAI",
"Use Horde": "Использовать Horde",
"API url": "API URL",
"API url": "URL-адрес API",
"PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Режим обёртки API OpenAI)",
"Register a Horde account for faster queue times": "Заведите учетную запись Horde для ускорения генерации",
"Learn how to contribute your idle GPU cycles to the Hord": "Узнайте подробнее о том, как использовать время простоя GPU для Hord",
"Adjust context size to worker capabilities": "Уточнить размер контекста в соответствии с возможностями рабочих машин",
"Adjust response length to worker capabilities": "Уточнить длинну ответа в соответствии с возможностями рабочих машин",
"API key": "API-ключ",
"Tabby API key": "Tabby API-ключ",
"Get it here:": "Получить здесь:",
"Register": "Регистрация",
"TogetherAI Model": "Модель TogetherAI",
"Example: 127.0.0.1:5001": "Пример: http://127.0.0.1:5001",
"ggerganov/llama.cpp": "ggerganov/llama.cpp (сервер вывода)",
"Example: 127.0.0.1:8080": "Пример: http://127.0.0.1:8080",
"Example: 127.0.0.1:11434": "Пример: http://127.0.0.1:11434",
"Ollama Model": "Модель Ollama",
"Download": "Скачать",
"TogetherAI API Key": "TogetherAI API-ключ",
"-- Connect to the API --": "-- Подключитесь к API --",
"View my Kudos": "Посмотреть мой рейтинг(Kudos)",
"Enter": "Вставьте",
"to use anonymous mode.": "чтобы использовать анонимный режим.",
@@ -1796,11 +1848,18 @@
"Novel AI Model": "Модель NovelAI",
"If you are using:": "Если вы используете:",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "Убедитесь, что при запуске указали аргумент --extensions openai",
"Make sure you run it with": "Убедитесь, что вы запустили его с",
"flag": "флажком",
"API key (optional)": "Ключ API (опционально)",
"Server url": "URL-адрес сервера",
"Custom model (optional)": "Пользовательская модель (опционально)",
"Bypass API status check": "Обход проверки статуса API",
"Mancer AI": "",
"Use API key (Only required for Mancer)": "Нажмите на ячейку (и добавьте свой API ключ!):",
"Blocking API url": "Блокирующий API url",
"Example: http://127.0.0.1:5000/api": "Пример: http://127.0.0.1:5000/api",
"Example: 127.0.0.1:5000": "Пример: http://127.0.0.1:5000",
"Legacy API (pre-OAI, no streaming)": "Устаревший API (до OAI, без потоковой передачи)",
"Bypass status check": "Обход проверки статуса",
"Streaming API url": "Потоковый API URL",
"Example: ws://127.0.0.1:5005/api/v1/stream": "Пример: ws://127.0.0.1:5005/api/v1/stream",
"Mancer API key": "Mancer API ключ",
@@ -1845,7 +1904,6 @@
"Chat Start": "Начало чата",
"Activation Regex": "Активация Regex",
"Instruct Mode": "Режим \"Инструктаж\"",
"Enabled": "Включен",
"Wrap Sequences with Newline": "Отделять последовательности красной строкой",
"Include Names": "Показывать имена",
"Force for Groups and Personas": "Усилия для Групп и Персон",
@@ -1859,11 +1917,21 @@
"System Sequence Suffix": "Суффикс системной последовательности",
"Stop Sequence": "Последовательность остановки",
"Context Formatting": "Форматирование контекста",
"(Saved to Context Template)": "(Сохраняется в шаблоне контекста)",
"Tokenizer": "Токенайзер",
"None / Estimated": "Отсутствует/Приблизительно",
"Sentencepiece (LLaMA)": "SentencepieceLLaMA",
"Sentencepiece (LLaMA)": "Sentencepiece (LLaMA)",
"Token Padding": "Заполнение токенов",
"Save preset as": "Сохранить предустановку как",
"Always add character's name to prompt": "Всегда добавлять имя персонажа в инструкции",
"Use as Stop Strings": "Использование в качестве стоп-строк",
"Bind to Context": "Привязка к контексту",
"Generate only one line per request": "Генерировать только одну строку для каждого запроса",
"Misc. Settings": "Доп. настройки",
"Auto-Continue": "Авто продолжение",
"Collapse Consecutive Newlines": "Свернуть последовательные новые строки",
"Allow for Chat Completion APIs": "Разрешить API завершения чата",
"Target length (tokens)": "Целевая длина (токены)",
"Keep Example Messages in Prompt": "Сохранять примеры сообщений в инструкции",
"Remove Empty New Lines from Output": "Удалять пустые строчки из вывода",
"Disabled for all models": "Выключено для всех моделей",
@@ -1876,6 +1944,11 @@
"Style Anchor": "Стиль Anchors",
"World Info": "Информация о мире",
"Scan Depth": "Глубина сканирования",
"Case-Sensitive": "С учетом регистра",
"Match Whole Words": "Сопоставить целые слова",
"Use global setting": "Использовать глобальную настройку",
"Yes": "Да",
"No": "Нет",
"Context %": "Процент контекста",
"Budget Cap": "Бюджетный лимит",
"(0 = disabled)": "(0 = отключено)",
@@ -1892,13 +1965,13 @@
"Avatar Style": "Стиль аватаров",
"Circle": "Круглые",
"Rectangle": "Прямоугольные",
"Square": "Квадратные",
"Chat Style": "Стиль чата",
"Default": "По умолчанию",
"Bubbles": "Пузыри",
"Chat Width (PC)": "Ширина чата (на PC",
"No Blur Effect": "Отключить эффект размытия",
"No Text Shadows": "Отключить тень текста",
"Waifu Mode": "!!!РЕЖИМ ВАЙФУ!!!",
"No Text Shadows": "Отключить тень от текста",
"Waifu Mode": "Рeжим Вайфу",
"Message Timer": "Таймер сообщений",
"Model Icon": "Показать значки модели",
"# of messages (0 = disabled)": "# сообщений (0 = отключено)",
@@ -1906,10 +1979,21 @@
"Allow {{char}}: in bot messages": "Показывать {{char}}: в ответах",
"Allow {{user}}: in bot messages": "Показать {{user}}: в ответах",
"Show tags in responses": "Показывать <теги> в ответах",
"Aux List Field": "Вспомогательное поле списка",
"Lorebook Import Dialog": "Импрот Lorebook-ка",
"MUI Preset": "Предустановка MUI:",
"If set in the advanced character definitions, this field will be displayed in the characters list.": "Если это поле задано в расширенных параметрах персонажа, оно будет отображаться в списке персонажа.",
"Relaxed API URLS": "Смягченные URL-адреса API",
"Custom CSS": "Пользовательский CSS",
"Default (oobabooga)": "По умолчанию (oobabooga)",
"Mancer Model": "Модель Mancer",
"API Type": "Тип API",
"Aphrodite API key": "API-ключ Aphrodite",
"Relax message trim in Groups": "Расслабленная отделка сообщений в Группах",
"Characters Hotswap": "Смена персонажей на лету",
"Request token probabilities": "Вероятность запроса токена",
"Movable UI Panels": "Перемещение панелей интерфейса",
"Reset Panels": "Сбросить панели",
"Reset Panels": "Сбросить MovingUI",
"UI Colors": "Цвета интерфейса",
"Main Text": "Основной текст",
"Italics Text": "Курсивный текст",
@@ -1923,6 +2007,8 @@
"UI Theme Preset": "Предустановки интерфейса",
"Power User Options": "Продвинутые параметры",
"Swipes": "Свайвы",
"Miscellaneous": "Разное",
"Theme Toggles": "Переключатели темы",
"Background Sound Only": "Только фоновый звук",
"Auto-load Last Chat": "Автоматически загружать последий чат",
"Auto-save Message Edits": "Автоматически сохранять отредактированные сообщения",
@@ -1935,6 +2021,15 @@
"Automatic (desktop)": "Автоматически (системные настройки)",
"Always enabled": "Всегда включена",
"Debug Menu": "Меню отладки",
"Restore User Input": "Восстановить запрос пользователя",
"Character Handling": "Обработка персонажа",
"Example Messages Behavior": "Пример поведения в сообщениях:",
"Gradual push-out": "Постепенное выталкивание",
"Chat/Message Handling": "Обработка чата/сообщения",
"Always include examples": "Всегда включать примеры",
"Never include examples": "Никогда не включать примеры",
"Forbid External Media": "Запрет внешних медиа",
"System Backgrounds": "Системные фоны",
"Name": "Имя",
"Your Avatar": "Ваш Аватар",
"Extensions API:": "API для расширений",
@@ -2039,14 +2134,32 @@
"Separator": "Разделитель",
"Start Reply With": "Начинать ответ с",
"Show reply prefix in chat": "Показывать префиксы ответов в чате",
"Worlds/Lorebooks": "Миры/Сведения",
"Worlds/Lorebooks": "Миры/Lorebook-ки",
"Active World(s)": "Активные миры",
"Activation Settings": "Настройки активации",
"Character Lore Insertion Strategy": "Порядок включения сведений",
"Sorted Evenly": "Равномерная сортировка",
"Active World(s) for all chats": "Активные миры для всех чатов",
"-- World Info not found --": "-- Информация о мире не найдена --",
"--- Pick to Edit ---": "Редактировать",
"--- Pick to Edit ---": "--- Редактировать ---",
"or": "или",
"New": "Новый",
"Priority": "Приритет",
"Custom": "Пользовательский",
"Title A-Z": "Название от A до Z",
"Title Z-A": "Название от Z до A",
"Tokens ↗": "Токены ↗",
"Tokens ↘": "Токены ↘",
"Depth ↗": "Глубина ↗",
"Depth ↘": "Глубина ↘",
"Order ↗": "Порядок ↗",
"Order ↘": "Порядок ↘",
"UID ↗": "Уник. ID ↗",
"UID ↘": "Уник. ID ↘",
"Trigger% ↗": "Триггер% ↗",
"Trigger% ↘": "Триггер% ↘",
"Order:": "Порядок:",
"Depth:": "Глубина:",
"Character Lore First": "Сначала сведения о персонаже",
"Global Lore First": "Сначала общие сведения",
"Recursive Scan": "Рекурсивное сканирование",
@@ -2058,27 +2171,47 @@
"Comma seperated (ignored if empty)": "Разделение запятыми (не используется, если оставлено пустым)",
"Use Probability": "Использовать вероятность",
"Exclude from recursion": "Исключить из рекурсии",
"Entry Title/Memo": "Вставьте Название/Заметку",
"Position:": "Положение:",
"Before Char Defs": "Перед определением Персонажа",
"After Char Defs": "После определения Персонажа",
"Before AN": "Перед AN",
"After AN": "После AN",
"Order:": "Порядок:",
"T_Position": "↑Char: Перед определениями Персонажа\n↓Char: После определений Персонажа\n↑AN: Перед Авторскими заметками\n↓AN: После Авторских заметок\n@D: На глубине",
"Before Char Defs": "↑Перс.",
"After Char Defs": "Перс.",
"Before AN": "↑АЗ",
"After AN": "↓АЗ",
"at Depth": "@Г",
"Order": "Порядок:",
"Probability:": "Вероятность:",
"Update a theme file": "Обновить файл темы",
"Save as a new theme": "Сохранить как новую тему",
"Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных слов в черном списке для запуска авто-свайпа.",
"Delete Entry": "Удалить запись:",
"User Message Blur Tint": "Оттенок размытия сообщения пользователя",
"AI Message Blur Tint": "Оттенок размытия сообщения ИИ",
"User Message Blur Tint": "Сообщение пользователя",
"AI Message Blur Tint": "Сообщение ИИ",
"Chat Backgrounds": "Фоны чата",
"Chat Background": "Фон чата",
"UI Background": "Фон интерфейса",
"Mad Lab Mode": "Режим безумца",
"Show Message Token Count": "Счетчик токенов сообщения",
"Compact Input Area (Mobile)": "Компактная зона ввода",
"Zen Sliders": "Дзен слайдеры",
"UI Border": "Границы интерфейса",
"Chat Style:": "Стиль чата",
"Chat Width (PC):": "Ширина чата (для ПК)",
"Chat Timestamps": "Временные обозначения в чате",
"Chat Width (PC)": "Ширина чата (для ПК)",
"Chat Timestamps": "Временные метки в чате",
"Tags as Folders": "Теги как папки",
"Chat Truncation": "Усечение чата",
"(0 = unlimited)": "(0 = неограниченное)",
"Streaming FPS": "Потоковый FPS",
"Gestures": "Жесты",
"Message IDs": "ID сообщений",
"Prefer Character Card Prompt": "Предпочитать инструкции из Карточки Персонажа",
"Prefer Character Card Jailbreak": "Предпочитать JailBreak из Карточки Персонажа",
"Press Send to continue": "Нажатие Отправить для продолжения",
"Prefer Character Card Jailbreak": "Предпочитать Джеилбреик из Карточки Персонажа",
"Press Send to continue": "Нажатие 'Отправить' для продолжения",
"Quick 'Continue' button": "Кнопка быстрого 'Продолжения'",
"Log prompts to console": "Выводы журнала в консоли",
"Never resize avatars": "Никогда не менять размер аватаров",
"Show avatar filenames": "Показывать названия файлов аватаров",
"Import Card Tags": "Импорт меток Карточки",
"Import Card Tags": "Импорт тегов Карточки",
"Confirm message deletion": "Подтверждение удаления сообщений",
"Spoiler Free Mode": "Режим без спойлеров",
"Auto-swipe": "Автоматические свайпы",
@@ -2086,6 +2219,57 @@
"Blacklisted words": "Запрещенные слова",
"Blacklisted word count to swipe": "Количество запрещенных слов для свайпа",
"Reload Chat": "Перезагрузить чат",
"Search Settings": "Поиск настроек",
"Disabled": "Отключено",
"Automatic (PC)": "Автоматическое (ПК)",
"Enabled": "Включено",
"Simple": "Простой",
"Advanced": "Расширенный",
"Disables animations and transitions": "Отключение анимаций и переходов.",
"removes blur from window backgrounds": "Убрать размытие с фона окон, чтобы ускорить рендеринг.",
"Remove text shadow effect": "Удаление эффекта тени от текста.",
"Reduce chat height, and put a static sprite behind the chat window": "Уменьшитm высоту чата и поместить статичный спрайт за окном чата.",
"Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список контекстных элементов 'Действия с сообщением' для сообщений чата, а не прятать их за '...'.",
"Alternative UI for numeric sampling parameters with fewer steps": "Альтернативный пользовательский интерфейс для числовых параметров выборки с меньшим количеством шагов.",
"Entirely unrestrict all numeric sampling parameters": "Полностью разграничить все числовые параметры выборки.",
"Time the AI's message generation, and show the duration in the chat log": "Время генерации сообщений ИИ и его показ в журнале чата.",
"Show a timestamp for each message in the chat log": "Показывать временную метку для каждого сообщения в журнале чата.",
"Show an icon for the API that generated the message": "Показать значок API, сгенерировавшего сообщение.",
"Show sequential message numbers in the chat log": "Показывать порядковые номера сообщений в журнале чата.",
"Show the number of tokens in each message in the chat log": "Показать количество токенов в каждом сообщении в журнале чата.",
"Single-row message input area. Mobile only, no effect on PC": "Однорядная область ввода сообщений. Только для мобильных устройств, на ПК не работает.",
"In the Character Management panel, show quick selection buttons for favorited characters": "На панели управления персонажами отображають кнопки быстрого выбора для избранных персонажей.",
"Show tagged character folders in the character list": "Отобразить теговые папки с персонажами в списке персонажей.",
"Play a sound when a message generation finishes": "Воспроизведение звука при завершении генерации сообщения.",
"Only play a sound when ST's browser tab is unfocused": "Воспроизводить звук только тогда, когда вкладка браузера ST не выбрана.",
"Reduce the formatting requirements on API URLs": "Снижение требований к форматированию URL-адресов API.",
"Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "Запросить импорт информации о мире/Lorebook для каждого нового персонажа со встроенным Lorebook. Если флажок снят, вместо этого будет показано короткое сообщение.",
"Restore unsaved user input on page refresh": "Восстановление несохраненного пользовательского запроса при обновлении страницы.",
"Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "Позволяет изменять положение некоторых элементов пользовательского интерфейса путем их перетаскивания. Только для ПК, на мобильных не работает.",
"MovingUI preset. Predefined/saved draggable positions": "Предварительная настройка MovingUI. Предопределенные/сохраненные позиции для перетаскивания.",
"Save movingUI changes to a new file": "Сохранение изменений перемещаемого пользовательского интерфейса в новый файл.",
"Apply a custom CSS style to all of the ST GUI": "Применить пользовательский стиль CSS ко всем элементам графического интерфейса ST.",
"Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "Использовать нечеткое сопоставление и искать символы в списке по всем полям данных, а не только по подстроке имени.",
"If checked and the character card contains a prompt override (System Prompt), use that instead": "Если установлен флажок и карточка персонажа содержит переопределение подсказки (Системная подсказка), будет использована она вместо изначальной.",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "Если установлен флажок и карточка персонажа содержит переопределение джейлбрейка (инструкция Истории сообщений), будет использована он вместо изначального.",
"Avoid cropping and resizing imported character images. When off, crop/resize to 400x600": "Избегать обрезки и изменения размера импортированных изображений персонажей. Если выключено, обрезать/изменить размер до 400x600.",
"Show actual file names on the disk, in the characters list display only": "Отображение фактических имен файлов на диске, только в списке персонажей.",
"Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored": "Запрос на импорт встроенных тегов карт при импорте персонажей. В противном случае встроенные теги игнорируются.",
"Hide character definitions from the editor panel behind a spoiler button": "Скрыть определения персонажей из панели редактора за кнопкой спойлера.",
"Show a button in the input area to ask the AI to continue (extend) its last message": "Показать кнопку в области ввода, чтобы попросить ИИ продолжить (продлить) его последнее сообщение.",
"Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "Показывать кнопки со стрелками на последнем сообщении в чате, чтобы генерировать альтернативные ответы ИИ. Как для ПК, так и для мобильных устройств.",
"Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "Позволяет использовать жесты смахивания на последнем сообщении в чате, чтобы вызвать альтернативную генерацию. Только для мобильных устройств, на ПК не работает.",
"Save edits to messages without confirmation as you type": "Сохранять правки в сообщениях без подтверждения при вводе текста.",
"Render LaTeX and AsciiMath equation notation in chat messages. Powered by KaTeX": "Отображение нотации уравнений LaTeX и AsciiMath в сообщениях чата. При поддержке KaTeX.",
"Disalow embedded media from other domains in chat messages": "Запретить встроенные медиафайлы из других доменов в сообщениях чата.",
"Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "Не кодировать символы < и > в тексте сообщения, что позволяет использовать подмножество HTML-разметки, а также Markdown.",
"Allow AI messages in groups to contain lines spoken by other group members": "Разрешить в групповых сообщениях AI содержать реплики, произнесенные другими членами группы.",
"Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",
"Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",
"Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить функцию автоматического пролистывания. Настройки в этом разделе действуют только при включенном автопролистывании.",
"If the generated message is shorter than this, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
"Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
"Auto-Expand Message Actions": "Развернуть контекстные элементы",
"Not Connected": "Не подключено",
"Persona Management": "Управление Персоной",
"Persona Description": "Описание Персоны",
@@ -2117,7 +2301,7 @@
"Prompt Overrides (For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct mode)": "Замещение инструкций (Для OpenAI/Claude/Scale API, Window/OpenRouter, и Режима Instruct)",
"Insert {{original}} into either box to include the respective default prompt from system settings.": "Внесите {{original}} в любое поле для внесения стандартных инструкций из системных настроек",
"Main Prompt": "Главные инструкции",
"Jailbreak": "JailBreak",
"Jailbreak": "Jailbreak",
"Creator's Metadata (Not sent with the AI prompt)": "Сведения о создателе (не отправляются ИИ с инструкциями)",
"Everything here is optional": "Всё в данных полях опционально",
"Created by": "Создано",
@@ -2133,7 +2317,7 @@
"Rep. Pen. Freq.": "Частота наказания за повторы",
"Rep. Pen. Presence": "Наличие наказания за повторы",
"Enter it in the box below:": "Введите в поле ниже:",
"separate with commas w/o space between": "разделять запятыми без пробелов между:",
"separate with commas w/o space between": "разделять запятыми без пробела",
"Document": "Документ",
"Suggest replies": "Предлагать ответы",
"Show suggested replies. Not all bots support this.": "Показывать предлагаемые ответы. Не все боты поддерживают это.",
@@ -2145,7 +2329,7 @@
"AI reply prefix": "Префикс Ответ ИИ",
"Custom Stopping Strings": "Настройка ограничивающий нитей",
"JSON serialized array of strings": "JSON ориентированный набор нитей",
"words you dont want generated separated by comma ','": "слова которые вы не хотите при генерации здесь, разделенные запятой",
"words you dont want generated separated by comma ','": "Слова, которые вы не хотите генерировать, разделяются запятыми ','",
"Extensions URL": "URL расширений ",
"API Key": "Ключ API",
"Enter your name": "Введите свое имя",
@@ -2168,9 +2352,14 @@
"Injection text (supports parameters)": "Текст включения (Поддерживает параметры)",
"Injection depth": "Глубина включения",
"Type here...": "Пишите здесь...",
"Comma separated (required)": "Разделено запятыми (Обязательно)",
"Comma separated (ignored if empty)": "Разделено запятыми (Игнорируется если пусто)",
"What this keyword should mean to the AI, sent verbatim": "Значение этого ключевого слова, отправляется ИИ дословно",
"Comma separated (required)": "Разделять через запятую (Обязательное)",
"Comma separated (ignored if empty)": "Разделять через запятую (Игнорируется если пусто)",
"What this keyword should mean to the AI, sent verbatim": "Что это ключевое слово должно означать для ИИ, отправляется дословно",
"Filter to Character(s)": "Фильтр к персонажу(ам)",
"Character Exclusion": "Исключение персонажей",
"Inclusion Group": "Инклюзивная группа",
"Only one entry with the same label will be activated": "Будет актив. только одна запись с одинаковой меткой",
"-- Characters not found --": "-- Персонаж не найден --",
"Not sent to the AI": "Не отправляется ИИ",
"(This will be the first message from the character that starts every chat)": "(Это будет первое сообщение от персонажа, когда вы начинаете новый чат)",
"Not connected to API!": "Нет подключения к API",
@@ -2178,16 +2367,18 @@
"AI Configuration panel will stay open": "Панель Настройки ИИ останется открытой",
"Update current preset": "Обновить текущую настройку",
"Create new preset": "Создать новую настройку",
"Import preset": "Внести настройку",
"Export preset": "Скачать настройку",
"Delete the preset": "Удалить настройку",
"Import preset": "Импорт предустановки",
"Export preset": "Экспорт предустановки",
"Delete the preset": "Удалить предустановку",
"Auto-select this preset for Instruct Mode": "Автоматический выбор этой предустановки для режима 'Инструктаж'.",
"Auto-select this preset on API connection": "Автоматический выбор этой предустановки при подключении к API.",
"NSFW block goes first in the resulting prompt": "НСФВ блокировка идет первой при отправки Промта",
"Enables OpenAI completion streaming": "Включить процесс генерации OpenAI",
"Wrap user messages in quotes before sending": "Заключить ответ Пользователя в кавычки",
"Restore default prompt": "Восстановить станндартный промт",
"New preset": "Новая настройка",
"Delete preset": "Удалить настройку",
"Restore default jailbreak": "Восстановить стандартный Джейлбрейк",
"Restore default jailbreak": "Восстановить стандартный джейлбрейк",
"Restore default reply": "Восстановить стандартный ответ",
"Restore defaul note": "Восстановить стандартную заметку",
"API Connections": "Соединения API",
@@ -2198,7 +2389,6 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Подверждает ваше соединение к API. Знайте, что за это снимут деньги с вашего счета.",
"Create New": "Создать новое",
"Edit": "Изменить",
"World Info": "Информация о Мире",
"Locked = World Editor will stay open": "Закреплено = Редактирование Мира останется открытым",
"Entries can activate other entries by mentioning their keywords": "Записи могут активировать другие записи если в них содержаться ключевые слова",
"Lookup for the entry keys in the context will respect the case": "Большая буква имеет значение при активации ключевого слова",
@@ -2206,26 +2396,37 @@
"Open all Entries": "Открыть все Записи",
"Close all Entries": "Закрыть все Записи",
"Create": "Создать",
"Import World Info": "Внести Информацию Мира",
"Export World Info": "Скачать Информацию Мира",
"Delete World Info": "Удалить Информацию Мира",
"Rename World Info": "Переименовать Информацию Мира",
"Import World Info": "Импортировать Мир",
"Export World Info": "Экспортировать Мир",
"Delete World Info": "Удалить Мир",
"Duplicate World Info": "Дублировать Мир",
"Rename World Info": "Переименовать Мир",
"Refresh": "Обновить",
"Primary Keywords": "Основные ключевые слова",
"Logic": "Логика",
"AND ANY": "И ЛЮБОЙ",
"AND ALL": ВСЕ",
"NOT ALL": "НЕ ВСЕ",
"NOT ANY": "НЕ ЛЮБОЙ",
"Optional Filter": "Дополнительный фильтр",
"New Entry": "Новая Запись",
"Fill empty Memo/Titles with Keywords": "Заполните пустые Заметки/Названия ключевыми словами",
"Save changes to a new theme file": "Сохранить изменения в новой теме",
"removes blur and uses alternative background color for divs": "убирает размытие и использует альтернативный фон для разделов",
"If checked and the character card contains a prompt override (System Prompt), use that instead.": "Если выбрано и карточка персонажа содержит собственный промт (Системный Промт), выберите это",
"If checked and the character card contains a jailbreak override (Post History Instruction), use that instead.": "Если выбрано и карточка персонажа содержит собственный Джейлбрейк (После Истории Инструкций), выберите это",
"AI Response Formatting": "Формат ответа ИИ",
"Change Background Image": "Изменить фон",
"Extensions": "Расширения",
"Click to set a new User Name": "Нажмите что бы выбрать новое имя Личности",
"Click to lock your selected persona to the current chat. Click again to remove the lock.": "Нажмите что бы закрепить выьранную личность к текущему чату",
"Click to set user name for all messages": "Нажмите что бы закрепить Личность для всех сообщений",
"Click to set a new User Name": "Нажмите, чтобы задать новое имя пользователя.",
"Click to lock your selected persona to the current chat. Click again to remove the lock.": "Нажмите, чтобы закрепить выбранную персону за текущим чатом. Нажмите еще раз, чтобы снять блокировку.",
"Click to set user name for all messages": "Нажмите, чтобы задать имя пользователя для всех сообщений.",
"Create a dummy persona": "Создать болванку",
"Character Management": "Управление Персонажами",
"Locked = Character Management panel will stay open": "Закреплено = Панель Управление Персонажами останется открытой ",
"Select/Create Characters": "Выбрать/Создать персонажа",
"Token counts may be inaccurate and provided just for reference.": "Счетчик токенов может быть неточным и используется только для примера",
"Click to select a new avatar for this character": "Нажмите что бы выбрать новый аватар для этого персонажа",
"Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]",
"Toggle grid view": "Переключить вид сетки",
"Add to Favorites": "Добавить в Любимые",
"Advanced Definition": "Расширенные Определения",
"Character Lore": "Сведения Персонажа",
@@ -2283,13 +2484,22 @@
"Add to group": "Добавить в группу",
"Add": "Добавить",
"Abort request": "Прекратить генерацию",
"Send a message": "отправить сообщение",
"Ask AI to write your message for you": "ИИ напишет сообщение за вас",
"Send a message": "Отправить сообщение",
"Ask AI to write your message for you": "Попросить ИИ написать для вас сообщение.",
"Continue the last message": "Продолжить текущее сообщение",
"Bind user name to that avatar": "Закрепить имя за этой личностью",
"Select this as default persona for the new chats.": "Выбрать эту как стартовую личность",
"Change persona image": "Сменить изображение личности",
"Delete persona": "Удалить личность"
"Bind user name to that avatar": "Закрепить имя за этой Персоной",
"Select this as default persona for the new chats.": "Выберать эту Персону в качестве персоны по умолчанию для новых чатов.",
"Change persona image": "Сменить аватар Персоны.",
"Delete persona": "Удалить Персону.",
"Reduced Motion": "Сокращение анимаций",
"Auto-select": "Авто выбор",
"Automatically select a background based on the chat context": "Автоматический выбор фона в зависимости от контекста чата",
"Filter": "Фильтр",
"Exclude message from prompts": "Исключить сообщение из подсказок",
"Include message in prompts": "Включить сообщение в подсказки",
"Create checkpoint": "Создание контрольной точки",
"Create Branch": "Создать Ветку",
"Embed file or image": "Вставить файл или изображение"
},
"it-it": {
"clickslidertips": "consigli per gli slider",

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,11 @@ import {
textgen_types,
getTextGenServer,
validateTextGenUrl,
parseTextgenLogprobs,
parseTabbyLogprobs,
} from './scripts/textgen-settings.js';
const { MANCER, TOGETHERAI, OOBA, APHRODITE, OLLAMA } = textgen_types;
const { MANCER, TOGETHERAI, OOBA, APHRODITE, OLLAMA, INFERMATICAI, OPENROUTER } = textgen_types;
import {
world_info,
@@ -194,7 +196,7 @@ import { createPersona, initPersonas, selectCurrentPersona, setPersonaDescriptio
import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
import { hideLoader, showLoader } from './scripts/loader.js';
import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
import { loadMancerModels, loadOllamaModels, loadTogetherAIModels } from './scripts/textgen-models.js';
import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels } from './scripts/textgen-models.js';
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags } from './scripts/chats.js';
import { initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros } from './scripts/macros.js';
@@ -394,6 +396,8 @@ export const event_types = {
GROUP_CHAT_DELETED: 'group_chat_deleted',
GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
GROUP_MEMBER_DRAFTED: 'group_member_drafted',
WORLD_INFO_ACTIVATED: 'world_info_activated',
TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
};
export const eventSource = new EventEmitter();
@@ -1053,6 +1057,12 @@ async function getStatusTextgen() {
} else if (textgen_settings.type === OLLAMA) {
loadOllamaModels(data?.data);
online_status = textgen_settings.ollama_model || 'Connected';
} else if (textgen_settings.type === INFERMATICAI) {
loadInfermaticAIModels(data?.data);
online_status = textgen_settings.infermaticai_model;
} else if (textgen_settings.type === OPENROUTER) {
loadOpenRouterModels(data?.data);
online_status = textgen_settings.openrouter_model;
} else {
online_status = data?.result;
}
@@ -1603,15 +1613,21 @@ function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
}
if (!isSystem) {
let regexPlacement;
if (isUser) {
regexPlacement = regex_placement.USER_INPUT;
} else if (ch_name !== name2) {
regexPlacement = regex_placement.SLASH_COMMAND;
} else {
regexPlacement = regex_placement.AI_OUTPUT;
function getRegexPlacement() {
try {
if (isUser) {
return regex_placement.USER_INPUT;
} else if (chat[messageId]?.extra?.type === 'narrator') {
return regex_placement.SLASH_COMMAND;
} else {
return regex_placement.AI_OUTPUT;
}
} catch {
return regex_placement.AI_OUTPUT;
}
}
const regexPlacement = getRegexPlacement();
const usableMessages = chat.map((x, index) => ({ message: x, index: index })).filter(x => !x.message.is_system);
const indexOf = usableMessages.findIndex(x => x.index === Number(messageId));
const depth = messageId >= 0 && indexOf !== -1 ? (usableMessages.length - indexOf - 1) : undefined;
@@ -2163,6 +2179,22 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh
};
}
const getGroupValue = () => {
if (typeof _group === 'string') {
return _group;
}
if (selected_group) {
const members = groups.find(x => x.id === selected_group)?.members;
const names = Array.isArray(members)
? members.map(m => characters.find(c => c.avatar === m)?.name).filter(Boolean).join(', ')
: '';
return names;
} else {
return _name2 ?? name2;
}
};
if (_replaceCharacterCard) {
const fields = getCharacterCardFields();
environment.charPrompt = fields.system || '';
@@ -2175,10 +2207,9 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh
}
// Must be substituted last so that they're replaced inside {{description}}
// TODO: evaluate macros recursively so we don't need to rely on substitution order
environment.user = _name1 ?? name1;
environment.char = _name2 ?? name2;
environment.group = environment.charIfNotGroup = _group ?? name2;
environment.group = environment.charIfNotGroup = getGroupValue();
environment.model = getGeneratingModel();
return evaluateMacros(content, environment);
@@ -2249,12 +2280,21 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
return generateFinished;
}
/**
* Executes slash commands and returns the new text and whether the generation was interrupted.
* @param {string} message Text to be sent
* @returns {Promise<boolean>} Whether the message sending was interrupted
*/
async function processCommands(message) {
if (!message || !message.trim().startsWith('/')) {
return false;
}
const previousText = String($('#send_textarea').val());
const result = await executeSlashCommands(message);
if (!result || typeof result !== 'object') {
return null;
return false;
}
const currentText = String($('#send_textarea').val());
@@ -2635,8 +2675,8 @@ class StreamingProcessor {
}
const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined;
await saveChatConditional();
saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg);
await saveChatConditional();
activateSendButtons();
showSwipeButtons();
setGenerationProgress(0);
@@ -2857,7 +2897,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
const interruptedByCommand = await processCommands($('#send_textarea').val());
const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
if (interruptedByCommand) {
//$("#send_textarea").val('').trigger('input');
@@ -3158,7 +3198,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
setFloatingPrompt();
// Add WI to prompt (and also inject WI to AN value via hijack)
let { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoDepth } = await getWorldInfoPrompt(chat2, this_max_context);
let { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoDepth } = await getWorldInfoPrompt(chat2, this_max_context, dryRun);
if (skipWIAN !== true) {
console.log('skipWIAN not active, adding WIAN');
@@ -4448,6 +4488,16 @@ function parseAndSaveLogprobs(data, continueFrom) {
// `sendOpenAIRequest`. `data` for these APIs is just a string with
// the text of the generated message, logprobs are not included.
return;
case 'textgenerationwebui':
switch (textgen_settings.type) {
case textgen_types.LLAMACPP: {
logprobs = data?.completion_probabilities?.map(x => parseTextgenLogprobs(x.content, [x])) || null;
} break;
case textgen_types.APHRODITE:
case textgen_types.TABBY: {
logprobs = parseTabbyLogprobs(data) || null;
} break;
} break;
default:
return;
}
@@ -5899,10 +5949,10 @@ function updateMessage(div) {
let regexPlacement;
if (mes.is_user) {
regexPlacement = regex_placement.USER_INPUT;
} else if (mes.name === name2) {
regexPlacement = regex_placement.AI_OUTPUT;
} else if (mes.name !== name2 || mes.extra?.type === 'narrator') {
} else if (mes.extra?.type === 'narrator') {
regexPlacement = regex_placement.SLASH_COMMAND;
} else {
regexPlacement = regex_placement.AI_OUTPUT;
}
// Ignore character override if sent as system
@@ -5917,7 +5967,11 @@ function updateMessage(div) {
text = text.trim();
}
const bias = extractMessageBias(text);
const bias = substituteParams(extractMessageBias(text));
text = substituteParams(text);
if (bias) {
text = removeMacros(text);
}
mes['mes'] = text;
if (mes['swipe_id'] !== undefined) {
mes['swipes'][mes['swipe_id']] = text;
@@ -5960,7 +6014,7 @@ function openMessageDelete(fromSlashCommand) {
}
function messageEditAuto(div) {
const { mesBlock, text, mes } = updateMessage(div);
const { mesBlock, text, mes, bias } = updateMessage(div);
mesBlock.find('.mes_text').val('');
mesBlock.find('.mes_text').val(messageFormatting(
@@ -5970,6 +6024,8 @@ function messageEditAuto(div) {
mes.is_user,
this_edit_mes_id,
));
mesBlock.find('.mes_bias').empty();
mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));
saveChatDebounced();
}
@@ -7654,6 +7710,16 @@ const CONNECT_API_MAP = {
button: '#api_button_openai',
source: chat_completion_sources.CUSTOM,
},
'infermaticai': {
selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui',
type: textgen_types.INFERMATICAI,
},
'openrouter-text': {
selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui',
type: textgen_types.OPENROUTER,
},
};
async function selectContextCallback(_, name) {
@@ -7743,7 +7809,13 @@ async function connectAPISlash(_, text) {
}
}
export async function processDroppedFiles(files) {
/**
* Imports supported files dropped into the app window.
* @param {File[]} files Array of files to process
* @param {boolean?} preserveFileNames Whether to preserve original file names
* @returns {Promise<void>}
*/
export async function processDroppedFiles(files, preserveFileNames = false) {
const allowedMimeTypes = [
'application/json',
'image/png',
@@ -7755,14 +7827,20 @@ export async function processDroppedFiles(files) {
for (const file of files) {
if (allowedMimeTypes.includes(file.type)) {
await importCharacter(file);
await importCharacter(file, preserveFileNames);
} else {
toastr.warning('Unsupported file type: ' + file.name);
}
}
}
async function importCharacter(file) {
/**
* Imports a character from a file.
* @param {File} file File to import
* @param {boolean?} preserveFileName Whether to preserve original file name
* @returns {Promise<void>}
*/
async function importCharacter(file, preserveFileName = false) {
const ext = file.name.match(/\.(\w+)$/);
if (!ext || !(['json', 'png', 'yaml', 'yml'].includes(ext[1].toLowerCase()))) {
return;
@@ -7773,6 +7851,7 @@ async function importCharacter(file) {
const formData = new FormData();
formData.append('avatar', file);
formData.append('file_type', format);
formData.append('preserve_file_name', String(preserveFileName));
const data = await jQuery.ajax({
type: 'POST',
@@ -7830,9 +7909,9 @@ async function importFromURL(items, files) {
}
}
async function doImpersonate() {
async function doImpersonate(_, prompt) {
$('#send_textarea').val('');
$('#option_impersonate').trigger('click', { fromSlashCommand: true });
$('#option_impersonate').trigger('click', { fromSlashCommand: true, additionalPrompt: prompt });
}
async function doDeleteChat() {
@@ -7999,7 +8078,7 @@ jQuery(async function () {
registerSlashCommand('dupe', DupeChar, [], ' duplicates the currently selected character', true, true);
registerSlashCommand('api', connectAPISlash, [], `<span class="monospace">(${Object.keys(CONNECT_API_MAP).join(', ')})</span> connect to an API`, true, true);
registerSlashCommand('impersonate', doImpersonate, ['imp'], ' calls an impersonation response', true, true);
registerSlashCommand('impersonate', doImpersonate, ['imp'], '<span class="monospace">[prompt]</span> calls an impersonation response, with an optional additional prompt', true, true);
registerSlashCommand('delchat', doDeleteChat, [], ' deletes the current chat', true, true);
registerSlashCommand('getchatname', doGetChatName, [], ' returns the name of the current chat file into the pipe', false, true);
registerSlashCommand('closechat', doCloseChat, [], ' closes the current chat', true, true);
@@ -8450,7 +8529,7 @@ jQuery(async function () {
throw new Error('Unsuccessful request.');
}
const data = response.json();
const data = await response.json();
if (data.error) {
throw new Error('Server returned an error.');
@@ -8462,6 +8541,7 @@ jQuery(async function () {
else {
if (characters[this_chid].chat == old_filename) {
characters[this_chid].chat = newName;
$('#selected_chat_pole').val(characters[this_chid].chat);
await createOrEditCharacter();
}
}
@@ -8572,6 +8652,16 @@ jQuery(async function () {
await writeSecret(SECRET_KEYS.OOBA, oobaKey);
}
const infermaticAIKey = String($('#api_key_infermaticai').val()).trim();
if (infermaticAIKey.length) {
await writeSecret(SECRET_KEYS.INFERMATICAI, infermaticAIKey);
}
const openRouterKey = String($('#api_key_openrouter-tg').val()).trim();
if (openRouterKey.length) {
await writeSecret(SECRET_KEYS.OPENROUTER, openRouterKey);
}
validateTextGenUrl();
startStatusLoading();
main_api = 'textgenerationwebui';
@@ -8647,6 +8737,13 @@ jQuery(async function () {
const fromSlashCommand = customData?.fromSlashCommand || false;
var id = $(this).attr('id');
// Check whether a custom prompt was provided via custom data (for example through a slash command)
const additionalPrompt = customData?.additionalPrompt?.trim() || undefined;
const buildOrFillAdditionalArgs = (args = {}) => ({
...args,
...(additionalPrompt !== undefined && { quiet_prompt: additionalPrompt, quietToLoud: true }),
});
if (id == 'option_select_chat') {
if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) {
await displayPastChats();
@@ -8682,7 +8779,7 @@ jQuery(async function () {
}
else {
is_send_press = true;
Generate('regenerate');
Generate('regenerate', buildOrFillAdditionalArgs());
}
}
}
@@ -8690,14 +8787,14 @@ jQuery(async function () {
else if (id == 'option_impersonate') {
if (is_send_press == false || fromSlashCommand) {
is_send_press = true;
Generate('impersonate');
Generate('impersonate', buildOrFillAdditionalArgs());
}
}
else if (id == 'option_continue') {
if (is_send_press == false || fromSlashCommand) {
is_send_press = true;
Generate('continue');
Generate('continue', buildOrFillAdditionalArgs());
}
}
@@ -9834,6 +9931,7 @@ jQuery(async function () {
<li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
<li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>JanitorAI character (direct link or id)<br>Example: <tt>https://janitorai.com/characters/ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li>Pygmalion.chat character (link)<br>Example: <tt>https://pygmalion.chat/character/a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li>More coming soon...</li>
<ul>`;
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });

View File

@@ -389,8 +389,10 @@ function RA_autoconnect(PrevApi) {
}
break;
case 'textgenerationwebui':
if ((textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER]) ||
(textgen_settings.type === textgen_types.TOGETHERAI && secret_state[SECRET_KEYS.TOGETHERAI])
if ((textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER])
|| (textgen_settings.type === textgen_types.TOGETHERAI && secret_state[SECRET_KEYS.TOGETHERAI])
|| (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI]
|| (textgen_settings.type === textgen_types.OPENROUTER && secret_state[SECRET_KEYS.OPENROUTER]))
) {
$('#api_button_textgenerationwebui').trigger('click');
}

View File

@@ -3,8 +3,9 @@ TODO:
*/
//const DEBUG_TONY_SAMA_FORK_MODE = true
import { getRequestHeaders, callPopup } from '../../../script.js';
import { deleteExtension, extensionNames, installExtension, renderExtensionTemplate } from '../../extensions.js';
import { getRequestHeaders, callPopup, processDroppedFiles } from '../../../script.js';
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplate } from '../../extensions.js';
import { executeSlashCommands } from '../../slash-commands.js';
import { getStringHash, isValidUrl } from '../../utils.js';
export { MODULE_NAME };
@@ -61,8 +62,8 @@ function downloadAssetsList(url) {
for (const i in availableAssets[assetType]) {
const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`;
let element = $('<button />', { id: elemId, type: 'button', class: 'asset-download-button menu_button' });
const label = $('<i class="fa-fw fa-solid fa-download fa-xl"></i>');
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>');
element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE)
@@ -90,6 +91,11 @@ function downloadAssetsList(url) {
};
const assetDelete = async function () {
if (assetType === 'character') {
toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
await executeSlashCommands(`/go ${asset['id']}`);
return;
}
element.off('click');
await deleteAsset(assetType, asset['id']);
label.removeClass('fa-check');
@@ -126,20 +132,27 @@ function downloadAssetsList(url) {
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
const description = DOMPurify.sanitize(asset['description'] || '');
const url = isValidUrl(asset['url']) ? asset['url'] : '';
const previewIcon = assetType == 'extension' ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
$('<i></i>')
const assetBlock = $('<i></i>')
.append(element)
.append(`<div class="flex-container flexFlowColumn">
<span class="flex-container alignitemscenter">
.append(`<div class="flex-container flexFlowColumn flexNoGap">
<span class="asset-name flex-container alignitemscenter">
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="Preview in browser">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
</span>
<span>${description}</span>
</div>`)
.appendTo(assetTypeMenu);
<small class="asset-description">
${description}
</small>
</div>`);
if (assetType === 'character') {
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);
}
assetTypeMenu.append(assetBlock);
}
assetTypeMenu.appendTo('#assets_menu');
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
@@ -186,6 +199,10 @@ function isAssetInstalled(assetType, filename) {
assetList = extensionNames.filter(x => x.startsWith(thirdPartyMarker)).map(x => x.replace(thirdPartyMarker, ''));
}
if (assetType == 'character') {
assetList = getContext().characters.map(x => x.avatar);
}
for (const i of assetList) {
//console.debug(DEBUG_PREFIX,i,filename)
if (i.includes(filename))
@@ -215,6 +232,13 @@ async function installAsset(url, assetType, filename) {
});
if (result.ok) {
console.debug(DEBUG_PREFIX, 'Download success.');
if (category === 'character') {
console.debug(DEBUG_PREFIX, 'Importing character ', filename);
const blob = await result.blob();
const file = new File([blob], filename, { type: blob.type });
await processDroppedFiles([file], true);
console.debug(DEBUG_PREFIX, 'Character downloaded.');
}
}
}
catch (err) {

View File

@@ -27,17 +27,14 @@
color: inherit;
}
.assets-list-div i {
.assets-list-div > i {
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
padding: 5px;
font-style: normal;
}
.assets-list-div i span {
margin-left: 10px;
gap: 5px;
}
.assets-list-div i span:first-of-type {
@@ -46,12 +43,11 @@
.asset-download-button {
position: relative;
width: 50px;
padding: 8px 16px;
border: none;
outline: none;
border-radius: 2px;
cursor: pointer;
filter: none !important;
}
.asset-download-button:active {
@@ -85,6 +81,21 @@
animation: asset-download-button-loading-spinner 1s ease infinite;
}
.asset-name .avatar {
--imgSize: 30px !important;
flex: unset;
width: var(--imgSize);
height: var(--imgSize);
}
.asset-name .avatar img {
width: var(--imgSize);
height: var(--imgSize);
border-radius: 50%;
object-fit: cover;
object-position: center center;
}
@keyframes asset-download-button-loading-spinner {
from {
transform: rotate(0turn);

View File

@@ -6,6 +6,7 @@ import { QuickReplySet } from '../src/QuickReplySet.js';
import { QuickReplySettings } from '../src/QuickReplySettings.js';
// eslint-disable-next-line no-unused-vars
import { SettingsUi } from '../src/ui/SettingsUi.js';
import { onlyUnique } from '../../../utils.js';
export class QuickReplyApi {
/**@type {QuickReplySettings}*/ settings;
@@ -191,6 +192,7 @@ export class QuickReplyApi {
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
* @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
* @returns {QuickReply} the new quick reply
*/
createQuickReply(setName, label, {
@@ -202,6 +204,7 @@ export class QuickReplyApi {
executeOnAi,
executeOnChatChange,
executeOnGroupMemberDraft,
automationId,
} = {}) {
const set = this.getSetByName(setName);
if (!set) {
@@ -217,6 +220,7 @@ export class QuickReplyApi {
qr.executeOnAi = executeOnAi ?? false;
qr.executeOnChatChange = executeOnChatChange ?? false;
qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
qr.automationId = automationId ?? '';
qr.onUpdate();
return qr;
}
@@ -236,6 +240,7 @@ export class QuickReplyApi {
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
* @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
* @returns {QuickReply} the altered quick reply
*/
updateQuickReply(setName, label, {
@@ -248,6 +253,7 @@ export class QuickReplyApi {
executeOnAi,
executeOnChatChange,
executeOnGroupMemberDraft,
automationId,
} = {}) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
@@ -262,6 +268,7 @@ export class QuickReplyApi {
qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
qr.automationId = automationId ?? qr.automationId;
qr.onUpdate();
return qr;
}
@@ -454,4 +461,20 @@ export class QuickReplyApi {
}
return set.qrList.map(it=>it.label);
}
/**
* Gets a list of all Automation IDs used by quick replies.
*
* @returns {String[]} array with all automation IDs used by quick replies
*/
listAutomationIds() {
return this
.listSets()
.flatMap(it => ({ set: it, qrs: this.listQuickReplies(it) }))
.map(it => it.qrs?.map(qr => this.getQrByLabel(it.set, qr)?.automationId))
.flat()
.filter(Boolean)
.filter(onlyUnique)
.map(String);
}
}

View File

@@ -70,6 +70,10 @@
<input type="checkbox" id="qr--executeOnGroupMemberDraft">
<span><i class="fa-solid fa-fw fa-people-group"></i> Execute before group member message</span>
</label>
<div class="flex-container alignItemsBaseline" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered.">
<small>Automation ID</small>
<input type="text" id="qr--automationId" class="text_pole flex1" placeholder="( None )">
</div>
</div>

View File

@@ -104,6 +104,7 @@ const loadSets = async () => {
qr.executeOnAi = slot.autoExecute_botMessage ?? false;
qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
qr.automationId = slot.automationId ?? false;
qr.contextList = (slot.contextMenu ?? []).map(it=>({
set: it.preset,
isChained: it.chain,
@@ -199,6 +200,8 @@ const init = async () => {
autoExec = new AutoExecuteHandler(settings);
eventSource.on(event_types.APP_READY, async()=>await finalizeInit());
window['quickReplyApi'] = quickReplyApi;
};
const finalizeInit = async () => {
log('executing startup');
@@ -244,3 +247,8 @@ const onGroupMemberDraft = async () => {
await autoExec.handleGroupMemberDraft();
};
eventSource.on(event_types.GROUP_MEMBER_DRAFTED, (...args) => executeIfReadyElseQueue(onGroupMemberDraft, args));
const onWIActivation = async (entries) => {
await autoExec.handleWIActivation(entries);
};
eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));

View File

@@ -82,4 +82,20 @@ export class AutoExecuteHandler {
];
await this.performAutoExecute(qrList);
}
/**
* @param {any[]} entries Set of activated entries
*/
async handleWIActivation(entries) {
if (!this.checkExecute() || !Array.isArray(entries) || entries.length === 0) return;
const automationIds = entries.map(entry => entry.automationId).filter(Boolean);
if (automationIds.length === 0) return;
const qrList = [
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId))).flat(),
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId)))?.flat() ?? []),
];
await this.performAutoExecute(qrList);
}
}

View File

@@ -31,6 +31,7 @@ export class QuickReply {
/**@type {Boolean}*/ executeOnAi = false;
/**@type {Boolean}*/ executeOnChatChange = false;
/**@type {Boolean}*/ executeOnGroupMemberDraft = false;
/**@type {String}*/ automationId = '';
/**@type {Function}*/ onExecute;
/**@type {Function}*/ onDelete;
@@ -359,6 +360,13 @@ export class QuickReply {
this.executeOnGroupMemberDraft = executeOnGroupMemberDraft.checked;
this.updateContext();
});
/**@type {HTMLInputElement}*/
const automationId = dom.querySelector('#qr--automationId');
automationId.value = this.automationId;
automationId.addEventListener('input', () => {
this.automationId = automationId.value;
this.updateContext();
});
/**@type {HTMLElement}*/
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
@@ -492,6 +500,7 @@ export class QuickReply {
executeOnAi: this.executeOnAi,
executeOnChatChange: this.executeOnChatChange,
executeOnGroupMemberDraft: this.executeOnGroupMemberDraft,
automationId: this.automationId,
};
}
}

View File

@@ -150,6 +150,7 @@ export class SlashCommandHandler {
executeOnAi: isTrueBoolean(args.bot),
executeOnChatChange: isTrueBoolean(args.load),
executeOnGroupMemberDraft: isTrueBoolean(args.group),
automationId: args.automationId ?? '',
},
);
} catch (ex) {
@@ -171,6 +172,7 @@ export class SlashCommandHandler {
executeOnAi: args.bot === undefined ? undefined : isTrueBoolean(args.bot),
executeOnChatChange: args.load === undefined ? undefined : isTrueBoolean(args.load),
executeOnGroupMemberDraft: args.group === undefined ? undefined : isTrueBoolean(args.group),
automationId: args.automationId ?? '',
},
);
} catch (ex) {

View File

@@ -5,9 +5,16 @@
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div id="open_regex_editor" class="menu_button">
<i class="fa-solid fa-pen-to-square"></i>
<span>Open Editor</span>
<div class="flex-container">
<div id="open_regex_editor" class="menu_button">
<i class="fa-solid fa-pen-to-square"></i>
<span>Open Editor</span>
</div>
<div id="import_regex" class="menu_button">
<i class="fa-solid fa-file-import"></i>
<span>Import Script</span>
</div>
<input type="file" id="import_regex_file" hidden accept="*.json" />
</div>
<hr />
<label>Saved Scripts</label>

View File

@@ -1,7 +1,7 @@
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
import { extension_settings } from '../../extensions.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { getSortableDelay, uuidv4 } from '../../utils.js';
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
import { resolveVariable } from '../../variables.js';
import { regex_placement, runRegexScript } from './engine.js';
@@ -93,6 +93,11 @@ async function loadRegexScripts() {
scriptHtml.find('.edit_existing_regex').on('click', async function () {
await onRegexEditorOpenClick(scriptHtml.attr('id'));
});
scriptHtml.find('.export_regex').on('click', async function () {
const fileName = `${script.scriptName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`;
const fileData = JSON.stringify(script, null, 4);
download(fileData, fileName, 'application/json');
});
scriptHtml.find('.delete_regex').on('click', async function () {
const confirm = await callPopup('Are you sure you want to delete this regex script?', 'confirm');
@@ -270,6 +275,35 @@ function runRegexCallback(args, value) {
return value;
}
/**
* Performs the import of the regex file.
* @param {File} file Input file
*/
async function onRegexImportFileChange(file) {
if (!file) {
toastr.error('No file provided.');
return;
}
try {
const fileText = await getFileText(file);
const regexScript = JSON.parse(fileText);
if (!regexScript.scriptName) {
throw new Error('No script name provided.');
}
extension_settings.regex.push(regexScript);
saveSettingsDebounced();
await loadRegexScripts();
toastr.success(`Regex script "${regexScript.scriptName}" imported.`);
} catch (error) {
console.log(error);
toastr.error('Invalid JSON file.');
return;
}
}
// Workaround for loading in sequence with other extensions
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
jQuery(async () => {
@@ -287,6 +321,14 @@ jQuery(async () => {
$('#open_regex_editor').on('click', function () {
onRegexEditorOpenClick(false);
});
$('#import_regex_file').on('change', async function () {
const inputElement = this instanceof HTMLInputElement && this;
await onRegexImportFileChange(inputElement.files[0]);
inputElement.value = '';
});
$('#import_regex').on('click', function () {
$('#import_regex_file').trigger('click');
});
$('#saved_regex_scripts').sortable({
delay: getSortableDelay(),

View File

@@ -7,10 +7,13 @@
<span class="regex-toggle-on fa-solid fa-toggle-on" title="Disable script"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off" title="Enable script"></span>
</label>
<div class="edit_existing_regex menu_button">
<div class="edit_existing_regex menu_button" title="Edit script">
<i class="fa-solid fa-pencil"></i>
</div>
<div class="delete_regex menu_button">
<div class="export_regex menu_button" title="Export script">
<i class="fa-solid fa-file-export"></i>
</div>
<div class="delete_regex menu_button" title="Delete script">
<i class="fa-solid fa-trash"></i>
</div>
</div>

View File

@@ -206,6 +206,7 @@ const defaultSettings = {
expand: false,
interactive_mode: false,
multimodal_captioning: false,
snap: false,
prompts: promptTemplates,
@@ -389,6 +390,7 @@ async function loadSettings() {
$('#sd_openai_quality').val(extension_settings.sd.openai_quality);
$('#sd_comfy_url').val(extension_settings.sd.comfy_url);
$('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
$('#sd_snap').prop('checked', extension_settings.sd.snap);
for (const style of extension_settings.sd.styles) {
const option = document.createElement('option');
@@ -398,23 +400,7 @@ async function loadSettings() {
$('#sd_style').append(option);
}
// Find a closest resolution option match for the current width and height
let resolutionId = null, minAspectDiff = Infinity, minResolutionDiff = Infinity;
for (const [id, resolution] of Object.entries(resolutionOptions)) {
const aspectDiff = Math.abs((resolution.width / resolution.height) - (extension_settings.sd.width / extension_settings.sd.height));
const resolutionDiff = Math.abs(resolution.width * resolution.height - extension_settings.sd.width * extension_settings.sd.height);
if (resolutionDiff < minResolutionDiff || (resolutionDiff === minResolutionDiff && aspectDiff < minAspectDiff)) {
resolutionId = id;
minAspectDiff = aspectDiff;
minResolutionDiff = resolutionDiff;
}
if (resolutionDiff === 0 && aspectDiff === 0) {
break;
}
}
const resolutionId = getClosestKnownResolution();
$('#sd_resolution').val(resolutionId);
toggleSourceControls();
@@ -423,6 +409,32 @@ async function loadSettings() {
await loadSettingOptions();
}
/**
* Find a closest resolution option match for the current width and height.
*/
function getClosestKnownResolution() {
let resolutionId = null;
let minTotalDiff = Infinity;
const targetAspect = extension_settings.sd.width / extension_settings.sd.height;
const targetResolution = extension_settings.sd.width * extension_settings.sd.height;
const diffs = Object.entries(resolutionOptions).map(([id, resolution]) => {
const aspectDiff = Math.abs((resolution.width / resolution.height) - targetAspect) / targetAspect;
const resolutionDiff = Math.abs(resolution.width * resolution.height - targetResolution) / targetResolution;
return { id, totalDiff: aspectDiff + resolutionDiff };
});
for (const { id, totalDiff } of diffs) {
if (totalDiff < minTotalDiff) {
minTotalDiff = totalDiff;
resolutionId = id;
}
}
return resolutionId;
}
async function loadSettingOptions() {
return Promise.all([
loadSamplers(),
@@ -475,6 +487,11 @@ function onMultimodalCaptioningInput() {
saveSettingsDebounced();
}
function onSnapInput() {
extension_settings.sd.snap = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onStyleSelect() {
const selectedStyle = String($('#sd_style').find(':selected').val());
const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
@@ -1659,7 +1676,7 @@ function processReply(str) {
str = str.replaceAll('“', '');
str = str.replaceAll('.', ',');
str = str.replaceAll('\n', ', ');
str = str.replace(/[^a-zA-Z0-9,:()]+/g, ' '); // Replace everything except alphanumeric characters and commas with spaces
str = str.replace(/[^a-zA-Z0-9,:()']+/g, ' '); // Replace everything except alphanumeric characters and commas with spaces
str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
str = str.trim();
@@ -1765,7 +1782,7 @@ function setTypeSpecificDimensions(generationType) {
const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
// Face images are always portrait (pun intended)
if (generationType == generationMode.FACE && aspectRatio >= 1) {
if ((generationType == generationMode.FACE || generationType == generationMode.FACE_MULTIMODAL) && aspectRatio >= 1) {
// Round to nearest multiple of 64
extension_settings.sd.height = Math.round(extension_settings.sd.width * 1.5 / 64) * 64;
}
@@ -1778,6 +1795,28 @@ function setTypeSpecificDimensions(generationType) {
}
}
if (extension_settings.sd.snap) {
// Force to use roughly the same pixel count as before rescaling
const prevPixelCount = prevSDHeight * prevSDWidth;
const newPixelCount = extension_settings.sd.height * extension_settings.sd.width;
if (prevPixelCount !== newPixelCount) {
const ratio = Math.sqrt(prevPixelCount / newPixelCount);
extension_settings.sd.height = Math.round(extension_settings.sd.height * ratio / 64) * 64;
extension_settings.sd.width = Math.round(extension_settings.sd.width * ratio / 64) * 64;
console.log(`Pixel counts after rescaling: ${prevPixelCount} -> ${newPixelCount} (ratio: ${ratio})`);
const resolution = resolutionOptions[getClosestKnownResolution()];
if (resolution) {
extension_settings.sd.height = resolution.height;
extension_settings.sd.width = resolution.width;
console.log('Snap to resolution', JSON.stringify(resolution));
} else {
console.warn('Snap to resolution failed, using custom dimensions');
}
}
}
return { height: prevSDHeight, width: prevSDWidth };
}
@@ -2349,7 +2388,7 @@ async function onComfyOpenWorkflowEditorClick() {
`);
$('#sd_comfy_workflow_editor_placeholder_list_custom').append(el);
el.find('.sd_comfy_workflow_editor_custom_find').val(placeholder.find);
el.find('.sd_comfy_workflow_editor_custom_find').on('input', function() {
el.find('.sd_comfy_workflow_editor_custom_find').on('input', function () {
placeholder.find = this.value;
el.find('.sd_comfy_workflow_editor_custom_final').text(`"%${this.value}%"`);
el.attr('data-placeholder', `${this.value}`);
@@ -2357,7 +2396,7 @@ async function onComfyOpenWorkflowEditorClick() {
saveSettingsDebounced();
});
el.find('.sd_comfy_workflow_editor_custom_replace').val(placeholder.replace);
el.find('.sd_comfy_workflow_editor_custom_replace').on('input', function() {
el.find('.sd_comfy_workflow_editor_custom_replace').on('input', function () {
placeholder.replace = this.value;
saveSettingsDebounced();
});
@@ -2379,7 +2418,7 @@ async function onComfyOpenWorkflowEditorClick() {
addPlaceholderDom(placeholder);
saveSettingsDebounced();
});
(extension_settings.sd.comfy_placeholders ?? []).forEach(placeholder=>{
(extension_settings.sd.comfy_placeholders ?? []).forEach(placeholder => {
addPlaceholderDom(placeholder);
});
checkPlaceholders();
@@ -2700,6 +2739,7 @@ jQuery(async () => {
$('#sd_openai_style').on('change', onOpenAiStyleSelect);
$('#sd_openai_quality').on('change', onOpenAiQualitySelect);
$('#sd_multimodal_captioning').on('input', onMultimodalCaptioningInput);
$('#sd_snap').on('input', onSnapInput);
$('.sd_settings .inline-drawer-toggle').on('click', function () {
initScrollHeight($('#sd_prompt_prefix'));

View File

@@ -26,6 +26,10 @@
<input id="sd_expand" type="checkbox" />
Auto-enhance prompts
</label>
<label for="sd_snap" class="checkbox_label" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
<input id="sd_snap" type="checkbox" />
Snap auto-adjusted resolutions
</label>
<label for="sd_source">Source</label>
<select id="sd_source">
<option value="extras">Extras API (local / remote)</option>

View File

@@ -11,7 +11,7 @@ import {
updateMessageBlock,
} from '../../../script.js';
import { extension_settings, getContext } from '../../extensions.js';
import { secret_state, writeSecret } from '../../secrets.js';
import { findSecret, secret_state, writeSecret } from '../../secrets.js';
import { splitRecursive } from '../../utils.js';
export const autoModeOptions = {
@@ -598,13 +598,18 @@ jQuery(() => {
'deeplx': 'http://127.0.0.1:1188/translate',
};
const popupText = `<h3>${optionText} API URL</h3><i>Example: <tt>${String(exampleURLs[extension_settings.translate.provider])}</tt></i>`;
const url = await callPopup(popupText, 'input');
if (url == false) {
const secretKey = extension_settings.translate.provider + '_url';
const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
const url = await callPopup(popupText, 'input', savedUrl);
if (url == false || url == '') {
return;
}
await writeSecret(extension_settings.translate.provider + '_url', url);
await writeSecret(secretKey, url);
toastr.success('API URL saved');
$('#translate_url_button').addClass('success');
});

View File

@@ -196,14 +196,18 @@ class AllTalkTtsProvider {
$('#narrator_voice').val(this.settings.narrator_voice_gen);
console.debug('AllTalkTTS: Settings loaded');
await this.initEndpoint();
}
async initEndpoint() {
try {
// Check if TTS provider is ready
this.setupEventListeners();
this.updateLanguageDropdown();
await this.checkReady();
await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server
await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready
this.updateNarratorVoicesDropdown();
this.updateLanguageDropdown();
this.setupEventListeners();
this.applySettingsToHTML();
updateStatus('Ready');
} catch (error) {
@@ -488,15 +492,14 @@ class AllTalkTtsProvider {
const modelSelect = document.getElementById('switch_model');
if (modelSelect) {
// Remove the event listener if it was previously added
modelSelect.removeEventListener('change', debouncedModelSelectChange);
// Add the debounced event listener
modelSelect.addEventListener('change', debouncedModelSelectChange);
$(modelSelect).off('change').on('change', debouncedModelSelectChange);
}
// DeepSpeed Listener
const deepspeedCheckbox = document.getElementById('deepspeed');
if (deepspeedCheckbox) {
deepspeedCheckbox.addEventListener('change', async (event) => {
$(deepspeedCheckbox).off('change').on('change', async (event) => {
const deepSpeedValue = event.target.checked ? 'True' : 'False';
// Set status to Processing
updateStatus('Processing');
@@ -522,7 +525,7 @@ class AllTalkTtsProvider {
// Low VRAM Listener
const lowVramCheckbox = document.getElementById('low_vram');
if (lowVramCheckbox) {
lowVramCheckbox.addEventListener('change', async (event) => {
$(lowVramCheckbox).off('change').on('change', async (event) => {
const lowVramValue = event.target.checked ? 'True' : 'False';
// Set status to Processing
updateStatus('Processing');
@@ -548,7 +551,7 @@ class AllTalkTtsProvider {
// Narrator Voice Dropdown Listener
const narratorVoiceSelect = document.getElementById('narrator_voice');
if (narratorVoiceSelect) {
narratorVoiceSelect.addEventListener('change', (event) => {
$(narratorVoiceSelect).off('change').on('change', (event) => {
this.settings.narrator_voice_gen = `${event.target.value}.wav`;
this.onSettingsChange(); // Save the settings after change
});
@@ -556,7 +559,7 @@ class AllTalkTtsProvider {
const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside');
if (textNotInsideSelect) {
textNotInsideSelect.addEventListener('change', (event) => {
$(textNotInsideSelect).off('change').on('change', (event) => {
this.settings.text_not_inside = event.target.value;
this.onSettingsChange(); // Save the settings after change
});
@@ -569,7 +572,7 @@ class AllTalkTtsProvider {
const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues'); // Access the checkbox from index.js
if (atNarratorSelect && textNotInsideSelect && narratorVoiceSelect) {
atNarratorSelect.addEventListener('change', (event) => {
$(atNarratorSelect).off('change').on('change', (event) => {
const isNarratorEnabled = event.target.value === 'true';
this.settings.narrator_enabled = isNarratorEnabled; // Update the setting here
textNotInsideSelect.disabled = !isNarratorEnabled;
@@ -605,7 +608,7 @@ class AllTalkTtsProvider {
const atGenerationMethodSelect = document.getElementById('at_generation_method');
const atNarratorEnabledSelect = document.getElementById('at_narrator_enabled');
if (atGenerationMethodSelect) {
atGenerationMethodSelect.addEventListener('change', (event) => {
$(atGenerationMethodSelect).off('change').on('change', (event) => {
const selectedMethod = event.target.value;
if (selectedMethod === 'streaming_enabled') {
@@ -626,7 +629,7 @@ class AllTalkTtsProvider {
// Listener for Language Dropdown
const languageSelect = document.getElementById('language_options');
if (languageSelect) {
languageSelect.addEventListener('change', (event) => {
$(languageSelect).off('change').on('change', (event) => {
this.settings.language = event.target.value;
this.onSettingsChange(); // Save the settings after change
});
@@ -635,7 +638,7 @@ class AllTalkTtsProvider {
// Listener for AllTalk Endpoint Input
const atServerInput = document.getElementById('at_server');
if (atServerInput) {
atServerInput.addEventListener('input', (event) => {
$(atServerInput).off('input').on('input', (event) => {
this.settings.provider_endpoint = event.target.value;
this.onSettingsChange(); // Save the settings after change
});
@@ -665,8 +668,7 @@ class AllTalkTtsProvider {
//#########################//
async onRefreshClick() {
await this.checkReady(); // Check if the TTS provider is ready
await this.loadSettings(this.settings); // Reload the settings
await this.initEndpoint();
// Additional actions as needed
}

View File

@@ -327,7 +327,7 @@ async function rearrangeChat(chat) {
}
// Get the most relevant messages, excluding the last few
const queryResults = await queryCollection(chatId, queryText, settings.query);
const queryResults = await queryCollection(chatId, queryText, settings.insert);
const queryHashes = queryResults.hashes.filter(onlyUnique);
const queriedMessages = [];
const insertedHashes = new Set();
@@ -530,10 +530,15 @@ async function queryCollection(collectionId, searchText, topK) {
return results;
}
/**
* Purges the vector index for a collection.
* @param {string} collectionId Collection ID to purge
* @returns <Promise<boolean>> True if deleted, false if not
*/
async function purgeVectorIndex(collectionId) {
try {
if (!settings.enabled_chats) {
return;
return true;
}
const response = await fetch('/api/vector/purge', {
@@ -549,9 +554,10 @@ async function purgeVectorIndex(collectionId) {
}
console.log(`Vectors: Purged vector index for collection ${collectionId}`);
return true;
} catch (error) {
console.error('Vectors: Failed to purge', error);
return false;
}
}
@@ -566,8 +572,11 @@ async function onPurgeClick() {
toastr.info('No chat selected', 'Purge aborted');
return;
}
await purgeVectorIndex(chatId);
toastr.success('Vector index purged', 'Purge successful');
if (await purgeVectorIndex(chatId)) {
toastr.success('Vector index purged', 'Purge successful');
} else {
toastr.error('Failed to purge vector index', 'Purge failed');
}
}
async function onViewStatsClick() {

View File

@@ -139,17 +139,23 @@ function renderTopLogprobs() {
const candidates = topLogprobs
.sort(([, logA], [, logB]) => logB - logA)
.map(([text, log]) => {
const probability = Math.exp(log);
sum += probability;
return [text, probability, log];
if (log < 0) {
const probability = Math.exp(log);
sum += probability;
return [text, probability, log];
}
else {
return [text, log, null];
}
});
candidates.push(['<others>', 1 - sum, 0]);
let matched = false;
for (const [token, probability, log] of candidates) {
const container = $('<button class="flex-container flexFlowColumn logprobs_top_candidate"></button>');
const tokenNormalized = String(token).replace(/^▁/g, ' ');
if (token === selectedToken) {
if (token === selectedToken || tokenNormalized === selectedToken) {
matched = true;
container.addClass('selected');
}
@@ -157,7 +163,9 @@ function renderTopLogprobs() {
const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token)}`);
const percentText = $('<span></span>').text(`${(probability * 100).toFixed(2)}%`);
container.append(tokenText, percentText);
container.attr('title', `logarithm: ${log}`);
if (log) {
container.attr('title', `logarithm: ${log}`);
}
addKeyboardProps(container);
if (token !== '<others>') {
container.click(() => onAlternativeClicked(state.selectedTokenLogprobs, token));
@@ -215,7 +223,7 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
const replaceIndex = messageLogprobs.findIndex(x => x === tokenLogprobs);
const tokens = messageLogprobs.slice(0, replaceIndex + 1).map(({ token }) => token);
tokens[replaceIndex] = alternative;
tokens[replaceIndex] = String(alternative).replace(/^▁/g, ' ');
const prefix = continueFrom || '';
const prompt = prefix + tokens.join('');
@@ -328,7 +336,7 @@ function createSwipe(messageId, prompt) {
* @returns {string}
*/
function toVisibleWhitespace(input) {
return input.replace(/ /g, '·').replace(/\n/g, '↵');
return input.replace(/ /g, '·').replace(/▁/g, '·').replace(/\n/g, '↵');
}
/**
@@ -347,6 +355,9 @@ function withVirtualWhitespace(text, span) {
if (text.match(/\s$/)) {
result.push($(document.createTextNode('\u200b')));
}
if (text.match(/^▁/)) {
result.unshift(document.createTextNode('\u200b'));
}
// line breaks are trickier. we don't currently handle consecutive line
// breaks or line breaks occuring in between non-whitespace characters, but
// tokenizers generally don't produce those anyway.
@@ -459,7 +470,7 @@ function convertTokenIdLogprobsToText(input) {
}
export function initLogprobs() {
const debouncedRender = debounce(renderAlternativeTokensView, 250);
const debouncedRender = debounce(renderAlternativeTokensView, 500);
$('#logprobsViewerClose').click(onToggleLogprobsPanel);
$('#option_toggle_logprobs').click(onToggleLogprobsPanel);
eventSource.on(event_types.CHAT_CHANGED, debouncedRender);

View File

@@ -119,7 +119,7 @@ const scale_max = 8191;
const claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
const claude_100k_max = 99000;
let ai21_max = 9200; //can easily fit 9k gpt tokens because j2's tokenizer is efficient af
const unlocked_max = 100 * 1024;
const unlocked_max = max_200k;
const oai_max_temp = 2.0;
const claude_max_temp = 1.0; //same as j2
const j2_max_topk = 10.0;
@@ -216,7 +216,7 @@ const default_settings = {
claude_model: 'claude-instant-v1',
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium',
mistralai_model: 'mistral-medium-latest',
custom_model: '',
custom_url: '',
custom_include_body: '',
@@ -285,7 +285,7 @@ const oai_settings = {
claude_model: 'claude-instant-v1',
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium',
mistralai_model: 'mistral-medium-latest',
custom_model: '',
custom_url: '',
custom_include_body: '',
@@ -351,8 +351,19 @@ function validateReverseProxy() {
}
}
/**
* Converts the Chat Completion object to an Instruct Mode prompt string.
* @param {object[]} messages Array of messages
* @param {string} type Generation type
* @returns {string} Text completion prompt
*/
function convertChatCompletionToInstruct(messages, type) {
messages = messages.filter(x => x.content !== oai_settings.new_chat_prompt && x.content !== oai_settings.new_example_chat_prompt);
const newChatPrompts = [
substituteParams(oai_settings.new_chat_prompt),
substituteParams(oai_settings.new_example_chat_prompt),
substituteParams(oai_settings.new_group_chat_prompt),
];
messages = messages.filter(x => !newChatPrompts.includes(x.content));
let chatMessagesText = '';
let systemPromptText = '';
@@ -670,10 +681,9 @@ export function isOpenRouterWithInstruct() {
async function populateChatHistory(messages, prompts, chatCompletion, type = null, cyclePrompt = null) {
chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory'));
let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || '';
// Reserve budget for new chat message
const newChat = selected_group ? oai_settings.new_group_chat_prompt : oai_settings.new_chat_prompt;
const newChatMessage = new Message('system', substituteParams(newChat, null, null, null, names), 'newMainChat');
const newChatMessage = new Message('system', substituteParams(newChat), 'newMainChat');
chatCompletion.reserveBudget(newChatMessage);
// Reserve budget for group nudge
@@ -767,7 +777,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
chatCompletion.add(new MessageCollection('dialogueExamples'), prompts.index('dialogueExamples'));
if (Array.isArray(messageExamples) && messageExamples.length) {
const newExampleChat = new Message('system', oai_settings.new_example_chat_prompt, 'newChat');
const newExampleChat = new Message('system', substituteParams(oai_settings.new_example_chat_prompt), 'newChat');
[...messageExamples].forEach((dialogue, dialogueIndex) => {
let examplesAdded = 0;
@@ -1655,13 +1665,13 @@ async function sendOpenAIRequest(type, messages, signal) {
const nameStopString = isImpersonate ? `\n${name2}:` : `\n${name1}:`;
const stopStringsLimit = 3; // 5 - 2 (nameStopString and new_chat_prompt)
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['stop'] = [nameStopString, oai_settings.new_chat_prompt, ...getCustomStoppingStrings(stopStringsLimit)];
generate_data['stop'] = [nameStopString, substituteParams(oai_settings.new_chat_prompt), ...getCustomStoppingStrings(stopStringsLimit)];
}
if (isAI21) {
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['count_pen'] = Number(oai_settings.count_pen);
generate_data['stop_tokens'] = [name1 + ':', oai_settings.new_chat_prompt, oai_settings.new_group_chat_prompt];
generate_data['stop_tokens'] = [name1 + ':', substituteParams(oai_settings.new_chat_prompt), substituteParams(oai_settings.new_group_chat_prompt)];
}
if (isMistral) {
@@ -2166,8 +2176,13 @@ class ChatCompletion {
let squashedMessages = [];
for (let message of this.messages.collection) {
// Force exclude empty messages
if (message.role === 'system' && !message.content) {
continue;
}
if (!excludeList.includes(message.identifier) && message.role === 'system' && !message.name) {
if (lastMessage && message.content && lastMessage.role === 'system') {
if (lastMessage && lastMessage.role === 'system') {
lastMessage.content += '\n' + message.content;
lastMessage.tokens = tokenHandler.count({ role: lastMessage.role, content: lastMessage.content });
}
@@ -3350,8 +3365,16 @@ async function onModelChange() {
}
if ($(this).is('#model_mistralai_select')) {
// Upgrade old mistral models to new naming scheme
// would have done this in loadOpenAISettings, but it wasn't updating on preset change?
if (value === 'mistral-medium' || value === 'mistral-small' || value === 'mistral-tiny') {
value = value + '-latest';
} else if (value === '') {
value = default_settings.mistralai_model;
}
console.log('MistralAI model changed to', value);
oai_settings.mistralai_model = value;
$('#model_mistralai_select').val(oai_settings.mistralai_model);
}
if (value && $(this).is('#model_custom_select')) {
@@ -3735,9 +3758,11 @@ async function testApiConnection() {
}
function reconnectOpenAi() {
setOnlineStatus('no_connection');
resultCheckStatus();
$('#api_button_openai').trigger('click');
if (main_api == 'openai') {
setOnlineStatus('no_connection');
resultCheckStatus();
$('#api_button_openai').trigger('click');
}
}
function onProxyPasswordShowClick() {
@@ -4198,11 +4223,7 @@ $(document).ready(async function () {
oai_settings.chat_completion_source = String($(this).find(':selected').val());
toggleChatCompletionForms();
saveSettingsDebounced();
if (main_api == 'openai') {
reconnectOpenAi();
}
reconnectOpenAi();
eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source);
});

View File

@@ -38,7 +38,7 @@ import { tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { countOccurrences, debounce, delay, download, getFileText, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
export {
loadPowerUserSettings,
@@ -682,6 +682,7 @@ async function CreateZenSliders(elmnt) {
sliderID == 'top_k_textgenerationwebui' ||
sliderID == 'top_k' ||
sliderID == 'rep_pen_slope' ||
sliderID == 'smoothing_factor_textgenerationwebui' ||
sliderID == 'min_length_textgenerationwebui') {
offVal = 0;
}
@@ -696,6 +697,9 @@ async function CreateZenSliders(elmnt) {
sliderID == 'encoder_rep_pen_textgenerationwebui' ||
sliderID == 'temp_textgenerationwebui' ||
sliderID == 'temp' ||
sliderID == 'min_temp_textgenerationwebui' ||
sliderID == 'max_temp_textgenerationwebui' ||
sliderID == 'dynatemp_exponent_textgenerationwebui' ||
sliderID == 'guidance_scale_textgenerationwebui' ||
sliderID == 'guidance_scale') {
offVal = 1;
@@ -703,6 +707,9 @@ async function CreateZenSliders(elmnt) {
if (sliderID == 'guidance_scale_textgenerationwebui') {
numSteps = 78;
}
if (sliderID == 'top_k_textgenerationwebui') {
sliderMin = 0;
}
//customize amt gen steps
if (sliderID !== 'amount_gen' && sliderID !== 'rep_pen_range_textgenerationwebui') {
stepScale = sliderRange / numSteps;
@@ -1981,10 +1988,51 @@ async function updateTheme() {
toastr.success('Theme saved.');
}
/**
* Exports the current theme to a file.
*/
async function exportTheme() {
const themeFile = await saveTheme(power_user.theme);
const fileName = `${themeFile.name}.json`;
download(JSON.stringify(themeFile, null, 4), fileName, 'application/json');
}
/**
* Imports a theme from a file.
* @param {File} file File to import.
* @returns {Promise<void>} A promise that resolves when the theme is imported.
*/
async function importTheme(file) {
if (!file) {
return;
}
const fileText = await getFileText(file);
const parsed = JSON.parse(fileText);
if (!parsed.name) {
throw new Error('Missing name');
}
if (themes.some(t => t.name === parsed.name)) {
throw new Error('Theme with that name already exists');
}
themes.push(parsed);
await applyTheme(parsed.name);
await saveTheme(parsed.name);
const option = document.createElement('option');
option.selected = true;
option.value = parsed.name;
option.innerText = parsed.name;
$('#themes').append(option);
saveSettingsDebounced();
}
/**
* Saves the current theme to the server.
* @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
* @returns {Promise<void>} A promise that resolves when the theme is saved.
* @returns {Promise<object>} A promise that resolves when the theme is saved.
*/
async function saveTheme(name = undefined) {
if (typeof name !== 'string') {
@@ -2056,6 +2104,8 @@ async function saveTheme(name = undefined) {
power_user.theme = name;
saveSettingsDebounced();
}
return theme;
}
async function saveMovingUI() {
@@ -3278,6 +3328,30 @@ $(document).ready(() => {
reloadCurrentChat();
});
$('#ui_preset_import_button').on('click', function () {
$('#ui_preset_import_file').trigger('click');
});
$('#ui_preset_import_file').on('change', async function() {
const inputElement = this instanceof HTMLInputElement && this;
try {
const file = inputElement?.files?.[0];
await importTheme(file);
} catch (error) {
console.error('Error importing UI theme', error);
toastr.error(String(error), 'Failed to import UI theme');
} finally {
if (inputElement) {
inputElement.value = null;
}
}
});
$('#ui_preset_export_button').on('click', async function () {
await exportTheme();
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);

View File

@@ -313,6 +313,8 @@ class PresetManager {
'type',
'custom_model',
'bypass_status_check',
'infermaticai_model',
'openrouter_model',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));

View File

@@ -16,6 +16,7 @@ export const SECRET_KEYS = {
SERPAPI: 'api_key_serpapi',
MISTRALAI: 'api_key_mistralai',
TOGETHERAI: 'api_key_togetherai',
INFERMATICAI: 'api_key_infermaticai',
CUSTOM: 'api_key_custom',
OOBA: 'api_key_ooba',
};
@@ -26,7 +27,7 @@ const INPUT_MAP = {
[SECRET_KEYS.OPENAI]: '#api_key_openai',
[SECRET_KEYS.NOVEL]: '#api_key_novel',
[SECRET_KEYS.CLAUDE]: '#api_key_claude',
[SECRET_KEYS.OPENROUTER]: '#api_key_openrouter',
[SECRET_KEYS.OPENROUTER]: '.api_key_openrouter',
[SECRET_KEYS.SCALE]: '#api_key_scale',
[SECRET_KEYS.AI21]: '#api_key_ai21',
[SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie',
@@ -37,6 +38,7 @@ const INPUT_MAP = {
[SECRET_KEYS.CUSTOM]: '#api_key_custom',
[SECRET_KEYS.TOGETHERAI]: '#api_key_togetherai',
[SECRET_KEYS.OOBA]: '#api_key_ooba',
[SECRET_KEYS.INFERMATICAI]: '#api_key_infermaticai',
};
async function clearSecret() {
@@ -124,6 +126,11 @@ export async function readSecretState() {
}
}
/**
* Finds a secret value by key.
* @param {string} key Secret key
* @returns {Promise<string | undefined>} Secret value, or undefined if keys are not exposed
*/
export async function findSecret(key) {
try {
const response = await fetch('/api/secrets/find', {
@@ -192,5 +199,5 @@ jQuery(async () => {
const warningElement = $(`[data-for="${id}"]`);
warningElement.toggle(value.length > 0);
});
$('#openrouter_authorize').on('click', authorizeOpenRouter);
$('.openrouter_authorize').on('click', authorizeOpenRouter);
});

View File

@@ -80,14 +80,23 @@ class SlashCommandParser {
const excludedFromRegex = ['sendas'];
const firstSpace = text.indexOf(' ');
const command = firstSpace !== -1 ? text.substring(1, firstSpace) : text.substring(1);
const args = firstSpace !== -1 ? text.substring(firstSpace + 1) : '';
let args = firstSpace !== -1 ? text.substring(firstSpace + 1) : '';
const argObj = {};
let unnamedArg;
if (args.length > 0) {
let match;
// Match unnamed argument
const unnamedArgPattern = /(?:\w+=(?:"(?:\\.|[^"\\])*"|\S+)\s*)*(.*)/s;
match = unnamedArgPattern.exec(args);
if (match !== null && match[1].length > 0) {
args = args.slice(0, -match[1].length);
unnamedArg = match[1].trim();
}
// Match named arguments
const namedArgPattern = /(\w+)=("(?:\\.|[^"\\])*"|\S+)/g;
let match;
while ((match = namedArgPattern.exec(args)) !== null) {
const key = match[1];
const value = match[2];
@@ -95,13 +104,6 @@ class SlashCommandParser {
argObj[key] = value.replace(/(^")|("$)/g, '');
}
// Match unnamed argument
const unnamedArgPattern = /(?:\w+=(?:"(?:\\.|[^"\\])*"|\S+)\s*)*(.*)/s;
match = unnamedArgPattern.exec(args);
if (match !== null) {
unnamedArg = match[1].trim();
}
// Excluded commands format in their own function
if (!excludedFromRegex.includes(command)) {
unnamedArg = getRegexedString(
@@ -138,7 +140,7 @@ const getSlashCommandsHelp = parser.getHelpString.bind(parser);
parser.addCommand('?', helpCommandCallback, ['help'], ' get help on macros, chat formatting and commands', true, true);
parser.addCommand('name', setNameCallback, ['persona'], '<span class="monospace">(name)</span> sets user name and persona avatar (if set)', true, true);
parser.addCommand('sync', syncCallback, [], ' syncs user name in user-attributed messages in the current chat', true, true);
parser.addCommand('sync', syncCallback, [], ' syncs the user persona in user-attributed messages in the current chat', true, true);
parser.addCommand('lock', bindCallback, ['bind'], ' locks/unlocks a persona (name and avatar) to the current chat', true, true);
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">(filename)</span> sets a background according to filename, partial names allowed', false, true);
parser.addCommand('sendas', sendMessageAs, [], ' sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": <tt>/sendas name="Chloe" Hello, guys!</tt>', true, true);
@@ -148,7 +150,7 @@ parser.addCommand('comment', sendCommentMessage, [], '<span class="monospace">(t
parser.addCommand('single', setStoryModeCallback, ['story'], ' sets the message style to single document mode without names or avatars visible', true, true);
parser.addCommand('bubble', setBubbleModeCallback, ['bubbles'], ' sets the message style to bubble chat mode', true, true);
parser.addCommand('flat', setFlatModeCallback, ['default'], ' sets the message style to flat chat mode', true, true);
parser.addCommand('continue', continueChatCallback, ['cont'], ' continues the last message in the chat', true, true);
parser.addCommand('continue', continueChatCallback, ['cont'], '<span class="monospace">[prompt]</span> continues the last message in the chat, with an optional additional prompt', true, true);
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> opens up a chat with the character or group by its name', true, true);
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> generates a system message using a specified prompt', true, true);
parser.addCommand('ask', askCharacter, [], '<span class="monospace">(prompt)</span> asks a specified character card a prompt', true, true);
@@ -1119,6 +1121,12 @@ function findCharacterIndex(name) {
(a, b) => a.includes(b),
];
const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
if (exactAvatarMatch !== -1) {
return exactAvatarMatch;
}
for (const matchType of matchTypes) {
const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
if (index !== -1) {
@@ -1160,7 +1168,7 @@ async function openChat(id) {
await reloadCurrentChat();
}
function continueChatCallback() {
function continueChatCallback(_, prompt) {
setTimeout(async () => {
try {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
@@ -1171,7 +1179,7 @@ function continueChatCallback() {
// Prevent infinite recursion
$('#send_textarea').val('').trigger('input');
$('#option_continue').trigger('click', { fromSlashCommand: true });
$('#option_continue').trigger('click', { fromSlashCommand: true, additionalPrompt: prompt });
}, 1);
return '';

View File

@@ -16,6 +16,8 @@
<li><tt>&lcub;&lcub;mesExamplesRaw&rcub;&rcub;</tt> unformatted Dialogue Examples <b>(only for Story String)</b></li>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> the Character's name</li>
<li><tt>&lcub;&lcub;group&rcub;&rcub;</tt> a comma-separated list of group member names or the character name in solo chats. Alias: &lcub;&lcub;charIfNotGroup&rcub;&rcub;</li>
<li><tt>&lcub;&lcub;model&rcub;&rcub;</tt> a text generation model name for the currently selected API. <b>Can be inaccurate!</b></li>
<li><tt>&lcub;&lcub;lastMessage&rcub;&rcub;</tt> - the text of the latest chat message.</li>
<li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> index # of the latest chat message. Useful for slash command batching.</li>
<li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</tt> - the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</li>

View File

@@ -1,9 +1,12 @@
import { callPopup, getRequestHeaders, setGenerationParamsFromPreset } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';
import { tokenizers } from './tokenizers.js';
let mancerModels = [];
let togetherModels = [];
let infermaticAIModels = [];
export let openRouterModels = [];
export async function loadOllamaModels(data) {
if (!Array.isArray(data)) {
@@ -52,6 +55,32 @@ export async function loadTogetherAIModels(data) {
}
}
export async function loadInfermaticAIModels(data) {
if (!Array.isArray(data)) {
console.error('Invalid Infermatic AI models data', data);
return;
}
infermaticAIModels = data;
if (!data.find(x => x.id === textgen_settings.infermaticai_model)) {
textgen_settings.infermaticai_model = data[0]?.id || '';
}
$('#model_infermaticai_select').empty();
for (const model of data) {
if (model.display_type === 'image') {
continue;
}
const option = document.createElement('option');
option.value = model.id;
option.text = model.id;
option.selected = model.id === textgen_settings.infermaticai_model;
$('#model_infermaticai_select').append(option);
}
}
export async function loadMancerModels(data) {
if (!Array.isArray(data)) {
console.error('Invalid Mancer models data', data);
@@ -74,6 +103,28 @@ export async function loadMancerModels(data) {
}
}
export async function loadOpenRouterModels(data) {
if (!Array.isArray(data)) {
console.error('Invalid OpenRouter models data', data);
return;
}
openRouterModels = data;
if (!data.find(x => x.id === textgen_settings.openrouter_model)) {
textgen_settings.openrouter_model = data[0]?.id || '';
}
$('#openrouter_model').empty();
for (const model of data) {
const option = document.createElement('option');
option.value = model.id;
option.text = model.id;
option.selected = model.id === textgen_settings.openrouter_model;
$('#openrouter_model').append(option);
}
}
function onMancerModelSelect() {
const modelId = String($('#mancer_model').val());
textgen_settings.mancer_model = modelId;
@@ -91,12 +142,28 @@ function onTogetherModelSelect() {
setGenerationParamsFromPreset({ max_length: model.context_length });
}
function onInfermaticAIModelSelect() {
const modelName = String($('#model_infermaticai_select').val());
textgen_settings.infermaticai_model = modelName;
$('#api_button_textgenerationwebui').trigger('click');
const model = infermaticAIModels.find(x => x.id === modelName);
setGenerationParamsFromPreset({ max_length: model.context_length });
}
function onOllamaModelSelect() {
const modelId = String($('#ollama_model').val());
textgen_settings.ollama_model = modelId;
$('#api_button_textgenerationwebui').trigger('click');
}
function onOpenRouterModelSelect() {
const modelId = String($('#openrouter_model').val());
textgen_settings.openrouter_model = modelId;
$('#api_button_textgenerationwebui').trigger('click');
const model = openRouterModels.find(x => x.id === modelId);
setGenerationParamsFromPreset({ max_length: model.context_length });
}
function getMancerModelTemplate(option) {
const model = mancerModels.find(x => x.id === option?.element?.value);
@@ -130,6 +197,39 @@ function getTogetherModelTemplate(option) {
`));
}
function getInfermaticAIModelTemplate(option) {
const model = infermaticAIModels.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
return $((`
<div class="flex-container flexFlowColumn">
<div><strong>${DOMPurify.sanitize(model.id)}</strong></div>
</div>
`));
}
function getOpenRouterModelTemplate(option) {
const model = openRouterModels.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
return $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.context_length} ctx | <small>${price}</small></div>
</div>
`));
}
async function downloadOllamaModel() {
try {
const serverUrl = textgen_settings.server_urls[textgen_types.OLLAMA];
@@ -171,10 +271,25 @@ async function downloadOllamaModel() {
}
}
export function getCurrentOpenRouterModelTokenizer() {
const modelId = textgen_settings.openrouter_model;
const model = openRouterModels.find(x => x.id === modelId);
switch (model?.architecture?.tokenizer) {
case 'Llama2':
return tokenizers.LLAMA;
case 'Mistral':
return tokenizers.MISTRAL;
default:
return tokenizers.OPENAI;
}
}
jQuery(function () {
$('#mancer_model').on('change', onMancerModelSelect);
$('#model_togetherai_select').on('change', onTogetherModelSelect);
$('#model_infermaticai_select').on('change', onInfermaticAIModelSelect);
$('#ollama_model').on('change', onOllamaModelSelect);
$('#openrouter_model').on('change', onOpenRouterModelSelect);
$('#ollama_download_model').on('click', downloadOllamaModel);
if (!isMobile()) {
@@ -198,5 +313,19 @@ jQuery(function () {
searchInputCssClass: 'text_pole',
width: '100%',
});
$('#model_infermaticai_select').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getInfermaticAIModelTemplate,
});
$('#openrouter_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getOpenRouterModelTemplate,
});
}
});

View File

@@ -1,4 +1,6 @@
import {
eventSource,
event_types,
getRequestHeaders,
getStoppingStrings,
max_context,
@@ -12,6 +14,7 @@ import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasList
import { power_user, registerDebugFunction } from './power-user.js';
import EventSourceStream from './sse-stream.js';
import { getCurrentOpenRouterModelTokenizer } from './textgen-models.js';
import { SENTENCEPIECE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
import { getSortableDelay, onlyUnique } from './utils.js';
@@ -31,9 +34,34 @@ export const textgen_types = {
TOGETHERAI: 'togetherai',
LLAMACPP: 'llamacpp',
OLLAMA: 'ollama',
INFERMATICAI: 'infermaticai',
OPENROUTER: 'openrouter',
};
const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP } = textgen_types;
const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, OPENROUTER } = textgen_types;
const LLAMACPP_DEFAULT_ORDER = [
'top_k',
'tfs_z',
'typical_p',
'top_p',
'min_p',
'temperature',
];
const OOBA_DEFAULT_ORDER = [
'temperature',
'dynamic_temperature',
'quadratic_sampling',
'top_k',
'top_p',
'typical_p',
'epsilon_cutoff',
'eta_cutoff',
'tfs',
'top_a',
'min_p',
'mirostat',
];
const BIAS_KEY = '#textgenerationwebui_api-settings';
// Maybe let it be configurable in the future?
@@ -42,6 +70,8 @@ const MANCER_SERVER_KEY = 'mancer_server';
const MANCER_SERVER_DEFAULT = 'https://neuro.mancer.tech';
let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT;
let TOGETHERAI_SERVER = 'https://api.together.xyz';
let INFERMATICAI_SERVER = 'https://api.totalgpt.ai';
let OPENROUTER_SERVER = 'https://openrouter.ai/api';
const SERVER_INPUTS = {
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
@@ -96,6 +126,8 @@ const settings = {
negative_prompt: '',
grammar_string: '',
banned_tokens: '',
sampler_priority: OOBA_DEFAULT_ORDER,
samplers: LLAMACPP_DEFAULT_ORDER,
//n_aphrodite: 1,
//best_of_aphrodite: 1,
ignore_eos_token_aphrodite: false,
@@ -106,7 +138,9 @@ const settings = {
type: textgen_types.OOBA,
mancer_model: 'mytholite',
togetherai_model: 'Gryphe/MythoMax-L2-13b',
infermaticai_model: '',
ollama_model: '',
openrouter_model: 'openrouter/auto',
legacy_api: false,
sampler_order: KOBOLDCPP_ORDER,
logit_bias: [],
@@ -170,6 +204,8 @@ const setting_names = [
//'log_probs_aphrodite',
//'prompt_log_probs_aphrodite'
'sampler_order',
'sampler_priority',
'samplers',
'n',
'logit_bias',
'custom_model',
@@ -204,6 +240,14 @@ export function getTextGenServer() {
return TOGETHERAI_SERVER;
}
if (settings.type === INFERMATICAI) {
return INFERMATICAI_SERVER;
}
if (settings.type === OPENROUTER) {
return OPENROUTER_SERVER;
}
return settings.server_urls[settings.type] ?? '';
}
@@ -227,8 +271,8 @@ async function selectPreset(name) {
function formatTextGenURL(value) {
try {
// Mancer/Together doesn't need any formatting (it's hardcoded)
if (settings.type === MANCER || settings.type === TOGETHERAI) {
// Mancer/Together/InfermaticAI doesn't need any formatting (it's hardcoded)
if (settings.type === MANCER || settings.type === TOGETHERAI || settings.type === INFERMATICAI || settings.type === OPENROUTER) {
return value;
}
@@ -261,6 +305,10 @@ function getTokenizerForTokenIds() {
return power_user.tokenizer;
}
if (settings.type === OPENROUTER) {
return getCurrentOpenRouterModelTokenizer();
}
return tokenizers.LLAMA;
}
@@ -422,7 +470,7 @@ function loadTextGenSettings(data, loadedSettings) {
* Sorts the sampler items by the given order.
* @param {any[]} orderArray Sampler order array.
*/
function sortItemsByOrder(orderArray) {
function sortKoboldItemsByOrder(orderArray) {
console.debug('Preset samplers order: ' + orderArray);
const $draggableItems = $('#koboldcpp_order');
@@ -433,6 +481,26 @@ function sortItemsByOrder(orderArray) {
}
}
function sortLlamacppItemsByOrder(orderArray) {
console.debug('Preset samplers order: ', orderArray);
const $container = $('#llamacpp_samplers_sortable');
orderArray.forEach((name) => {
const $item = $container.find(`[data-name="${name}"]`).detach();
$container.append($item);
});
}
function sortOobaItemsByOrder(orderArray) {
console.debug('Preset samplers order: ', orderArray);
const $container = $('#sampler_priority_container');
orderArray.forEach((name) => {
const $item = $container.find(`[data-name="${name}"]`).detach();
$container.append($item);
});
}
jQuery(function () {
$('#koboldcpp_order').sortable({
delay: getSortableDelay(),
@@ -449,7 +517,47 @@ jQuery(function () {
$('#koboldcpp_default_order').on('click', function () {
settings.sampler_order = KOBOLDCPP_ORDER;
sortItemsByOrder(settings.sampler_order);
sortKoboldItemsByOrder(settings.sampler_order);
saveSettingsDebounced();
});
$('#llamacpp_samplers_sortable').sortable({
delay: getSortableDelay(),
stop: function () {
const order = [];
$('#llamacpp_samplers_sortable').children().each(function () {
order.push($(this).data('name'));
});
settings.samplers = order;
console.log('Samplers reordered:', settings.samplers);
saveSettingsDebounced();
},
});
$('#llamacpp_samplers_default_order').on('click', function () {
sortLlamacppItemsByOrder(LLAMACPP_DEFAULT_ORDER);
settings.samplers = LLAMACPP_DEFAULT_ORDER;
console.log('Default samplers order loaded:', settings.samplers);
saveSettingsDebounced();
});
$('#sampler_priority_container').sortable({
delay: getSortableDelay(),
stop: function () {
const order = [];
$('#sampler_priority_container').children().each(function () {
order.push($(this).data('name'));
});
settings.sampler_priority = order;
console.log('Samplers reordered:', settings.sampler_priority);
saveSettingsDebounced();
},
});
$('#textgenerationwebui_default_order').on('click', function () {
sortOobaItemsByOrder(OOBA_DEFAULT_ORDER);
settings.sampler_priority = OOBA_DEFAULT_ORDER;
console.log('Default samplers order loaded:', settings.sampler_priority);
saveSettingsDebounced();
});
@@ -536,12 +644,13 @@ jQuery(function () {
'penalty_alpha_textgenerationwebui': 0,
'typical_p_textgenerationwebui': 1, // Added entry
'guidance_scale_textgenerationwebui': 1,
'smoothing_factor_textgenerationwebui': 0,
};
for (const [id, value] of Object.entries(inputs)) {
const inputElement = $(`#${id}`);
if (inputElement.prop('type') === 'checkbox') {
inputElement.prop('checked', value);
inputElement.prop('checked', value).trigger('input');
} else if (inputElement.prop('type') === 'number') {
inputElement.val(value).trigger('input');
} else {
@@ -615,11 +724,25 @@ function setSettingByName(setting, value, trigger) {
if ('sampler_order' === setting) {
value = Array.isArray(value) ? value : KOBOLDCPP_ORDER;
sortItemsByOrder(value);
sortKoboldItemsByOrder(value);
settings.sampler_order = value;
return;
}
if ('sampler_priority' === setting) {
value = Array.isArray(value) ? value : OOBA_DEFAULT_ORDER;
sortOobaItemsByOrder(value);
settings.sampler_priority = value;
return;
}
if ('samplers' === setting) {
value = Array.isArray(value) ? value : LLAMACPP_DEFAULT_ORDER;
sortLlamacppItemsByOrder(value);
settings.samplers = value;
return;
}
if ('logit_bias' === setting) {
settings.logit_bias = Array.isArray(value) ? value : [];
return;
@@ -694,7 +817,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
} else {
const newText = data?.choices?.[0]?.text || data?.content || '';
text += newText;
logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs);
logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
}
yield { text, swipes, logprobs };
@@ -710,7 +833,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
* @param {Object} logprobs - logprobs object returned from the API
* @returns {import('logprobs.js').TokenLogprobs | null} - converted logprobs
*/
function parseTextgenLogprobs(token, logprobs) {
export function parseTextgenLogprobs(token, logprobs) {
if (!logprobs) {
return null;
}
@@ -727,11 +850,37 @@ function parseTextgenLogprobs(token, logprobs) {
const candidates = Object.entries(topLogprobs[0]);
return { token, topLogprobs: candidates };
}
case LLAMACPP: {
/** @type {Record<string, number>[]} */
if (!logprobs?.length) {
return null;
}
const candidates = logprobs[0].probs.map(x => [ x.tok_str, x.prob ]);
return { token, topLogprobs: candidates };
}
default:
return null;
}
}
export function parseTabbyLogprobs(data) {
const text = data?.choices?.[0]?.text;
const offsets = data?.choices?.[0]?.logprobs?.text_offset;
if (!text || !offsets) {
return null;
}
// Convert string offsets list to tokens
const tokens = offsets?.map((offset, index) => {
const nextOffset = offsets[index + 1] || text.length;
return text.substring(offset, nextOffset);
});
const topLogprobs = data?.choices?.[0]?.logprobs?.top_logprobs?.map(x => ({ top_logprobs: [x] }));
return tokens?.map((token, index) => parseTextgenLogprobs(token, topLogprobs[index])) || null;
}
/**
* Parses errors in streaming responses and displays them in toastr.
* @param {Response} response - Response from the server.
@@ -781,6 +930,14 @@ function getModel() {
return settings.togetherai_model;
}
if (settings.type === INFERMATICAI) {
return settings.infermaticai_model;
}
if (settings.type === OPENROUTER) {
return settings.openrouter_model;
}
if (settings.type === APHRODITE) {
return online_status;
}
@@ -827,6 +984,8 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0,
'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : 1,
'smoothing_factor': settings.smoothing_factor,
'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,
'samplers': settings.type === LLAMACPP ? settings.samplers : undefined,
'stopping_strings': getStoppingStrings(isImpersonate, isContinue),
'stop': getStoppingStrings(isImpersonate, isContinue),
'truncation_length': max_context,
@@ -867,6 +1026,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'n_predict': maxTokens,
'mirostat': settings.mirostat_mode,
'ignore_eos': settings.ban_eos_token,
'n_probs': power_user.request_token_probabilities ? 10 : undefined,
};
const aphroditeParams = {
'n': canMultiSwipe ? settings.n : 1,
@@ -906,6 +1066,8 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
params = Object.assign(params, llamaCppParams);
}
eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
return params;
}

View File

@@ -5,8 +5,9 @@ import { groups, selected_group } from './group-chats.js';
import { getStringHash } from './utils.js';
import { kai_flags } from './kai-settings.js';
import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
import { getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js';
const { OOBA, TABBY, KOBOLDCPP, APHRODITE, LLAMACPP } = textgen_types;
const { OOBA, TABBY, KOBOLDCPP, APHRODITE, LLAMACPP, OPENROUTER } = textgen_types;
export const CHARACTERS_PER_TOKEN_RATIO = 3.35;
const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
@@ -202,6 +203,9 @@ export function getTokenizerBestMatch(forApi) {
if (forApi === 'textgenerationwebui' && isTokenizerSupported) {
return tokenizers.API_TEXTGENERATIONWEBUI;
}
if (forApi === 'textgenerationwebui' && textgen_settings.type === OPENROUTER) {
return getCurrentOpenRouterModelTokenizer();
}
}
return tokenizers.LLAMA;
@@ -349,8 +353,11 @@ export function getTokenizerModel() {
}
// And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer)
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model) {
const model = model_list.find(x => x.id === oai_settings.openrouter_model);
if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model ||
main_api == 'textgenerationwebui' && textgen_settings.type === OPENROUTER && textgen_settings.openrouter_model) {
const model = main_api == 'openai'
? model_list.find(x => x.id === oai_settings.openrouter_model)
: openRouterModels.find(x => x.id === textgen_settings.openrouter_model);
if (model?.architecture?.tokenizer === 'Llama2') {
return llamaTokenizer;

View File

@@ -70,7 +70,7 @@ const SORT_ORDER_KEY = 'world_info_sort_order';
const METADATA_KEY = 'world_info';
const DEFAULT_DEPTH = 4;
const MAX_SCAN_DEPTH = 100;
const MAX_SCAN_DEPTH = 1000;
/**
* Represents a scanning buffer for one evaluation of World Info.
@@ -113,6 +113,10 @@ class WorldInfoBuffer {
if (messages[depth]) {
this.#depthBuffer[depth] = messages[depth].trim();
}
// break if last message is reached
if (depth === messages.length - 1) {
break;
}
}
}
@@ -226,14 +230,27 @@ const world_info_position = {
const worldInfoCache = {};
async function getWorldInfoPrompt(chat2, maxContext) {
/**
* Gets the world info based on chat messages.
* @param {string[]} chat The chat messages to scan.
* @param {number} maxContext The maximum context size of the generation.
* @param {boolean} isDryRun If true, the function will not emit any events.
* @typedef {{worldInfoString: string, worldInfoBefore: string, worldInfoAfter: string, worldInfoDepth: any[]}} WIPromptResult
* @returns {Promise<WIPromptResult>} The world info string and depth.
*/
async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
const activatedWorldInfo = await checkWorldInfo(chat2, maxContext);
const activatedWorldInfo = await checkWorldInfo(chat, maxContext);
worldInfoBefore = activatedWorldInfo.worldInfoBefore;
worldInfoAfter = activatedWorldInfo.worldInfoAfter;
worldInfoString = worldInfoBefore + worldInfoAfter;
if (!isDryRun && activatedWorldInfo.allActivatedEntries && activatedWorldInfo.allActivatedEntries.size > 0) {
const arg = Array.from(activatedWorldInfo.allActivatedEntries);
await eventSource.emit(event_types.WORLD_INFO_ACTIVATED, arg);
}
return {
worldInfoString,
worldInfoBefore,
@@ -922,6 +939,7 @@ const originalDataKeyMap = {
'matchWholeWords': 'extensions.match_whole_words',
'caseSensitive': 'extensions.case_sensitive',
'scanDepth': 'extensions.scan_depth',
'automationId': 'extensions.automation_id',
};
function setOriginalDataValue(data, uid, key, value) {
@@ -1268,6 +1286,7 @@ function getWorldEntry(name, data, entry) {
saveWorldInfo(name, data);
});
groupInput.val(entry.group ?? '').trigger('input');
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data)), 1);
// probability
if (entry.probability === undefined) {
@@ -1513,11 +1532,13 @@ function getWorldEntry(name, data, entry) {
// Clamp if necessary
if (value < 0) {
$(this).val(0).trigger('input');
toastr.warning('Scan depth cannot be negative');
return;
}
if (value > MAX_SCAN_DEPTH) {
$(this).val(MAX_SCAN_DEPTH).trigger('input');
toastr.warning(`Scan depth cannot exceed ${MAX_SCAN_DEPTH}`);
return;
}
@@ -1553,6 +1574,20 @@ function getWorldEntry(name, data, entry) {
});
matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
// automation id
const automationIdInput = template.find('input[name="automationId"]');
automationIdInput.data('uid', entry.uid);
automationIdInput.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).val();
data.entries[uid].automationId = value;
setOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
saveWorldInfo(name, data);
});
automationIdInput.val(entry.automationId ?? '').trigger('input');
setTimeout(() => createEntryInputAutocomplete(automationIdInput, getAutomationIdCallback(data)), 1);
template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed
function updatePosOrdDisplay(uid) {
@@ -1582,6 +1617,83 @@ function getWorldEntry(name, data, entry) {
return template;
}
/**
* Get the inclusion groups for the autocomplete.
* @param {any} data WI data
* @returns {(input: any, output: any) => any} Callback function for the autocomplete
*/
function getInclusionGroupCallback(data) {
return function(input, output) {
const groups = new Set();
for (const entry of Object.values(data.entries)) {
if (entry.group) {
groups.add(String(entry.group));
}
}
const haystack = Array.from(groups);
haystack.sort((a, b) => a.localeCompare(b));
const needle = input.term.toLowerCase();
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
const result = haystack.filter(x => x.toLowerCase().includes(needle));
if (input.term && !hasExactMatch) {
result.unshift(input.term);
}
output(result);
};
}
function getAutomationIdCallback(data) {
return function(input, output) {
const ids = new Set();
for (const entry of Object.values(data.entries)) {
if (entry.automationId) {
ids.add(String(entry.automationId));
}
}
if ('quickReplyApi' in window) {
// @ts-ignore
for (const automationId of window['quickReplyApi'].listAutomationIds()) {
ids.add(String(automationId));
}
}
const haystack = Array.from(ids);
haystack.sort((a, b) => a.localeCompare(b));
const needle = input.term.toLowerCase();
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
const result = haystack.filter(x => x.toLowerCase().includes(needle));
if (input.term && !hasExactMatch) {
result.unshift(input.term);
}
output(result);
};
}
/**
* Create an autocomplete for the inclusion group.
* @param {JQuery<HTMLElement>} input Input element to attach the autocomplete to
* @param {(input: any, output: any) => any} callback Source data callbacks
*/
function createEntryInputAutocomplete(input, callback) {
$(input).autocomplete({
minLength: 0,
source: callback,
select: function (event, ui) {
$(input).val(ui.item.value).trigger('input').trigger('blur');
},
});
$(input).on('focus click', function () {
$(input).autocomplete('search', String($(input).val()));
});
}
async function deleteWorldInfoEntry(data, uid) {
if (!data || !('entries' in data)) {
return;
@@ -1614,6 +1726,7 @@ const newEntryTemplate = {
scanDepth: null,
caseSensitive: null,
matchWholeWords: null,
automationId: '',
};
function createWorldInfoEntry(name, data, fromSlashCommand = false) {
@@ -1890,6 +2003,13 @@ async function getSortedEntries() {
}
}
/**
* Performs a scan on the chat and returns the world info activated.
* @param {string[]} chat The chat messages to scan.
* @param {number} maxContext The maximum context size of the generation.
* @typedef {{ worldInfoBefore: string, worldInfoAfter: string, WIDepthEntries: any[], allActivatedEntries: Set<any> }} WIActivated
* @returns {Promise<WIActivated>} The world info activated.
*/
async function checkWorldInfo(chat, maxContext) {
const context = getContext();
const buffer = new WorldInfoBuffer(chat);
@@ -1926,7 +2046,7 @@ async function checkWorldInfo(chat, maxContext) {
const sortedEntries = await getSortedEntries();
if (sortedEntries.length === 0) {
return { worldInfoBefore: '', worldInfoAfter: '' };
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], allActivatedEntries: new Set() };
}
while (needsToScan) {
@@ -2173,7 +2293,7 @@ async function checkWorldInfo(chat, maxContext) {
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan);
}
return { worldInfoBefore, worldInfoAfter, WIDepthEntries };
return { worldInfoBefore, worldInfoAfter, WIDepthEntries, allActivatedEntries };
}
/**
@@ -2367,6 +2487,7 @@ function convertCharacterBook(characterBook) {
scanDepth: entry.extensions?.scan_depth ?? null,
caseSensitive: entry.extensions?.case_sensitive ?? null,
matchWholeWords: entry.extensions?.match_whole_words ?? null,
automationId: entry.extensions?.automation_id ?? '',
};
});
@@ -2801,4 +2922,14 @@ jQuery(() => {
closeOnSelect: false,
});
}
$('#WorldInfo').on('scroll', () => {
$('.world_entry input[name="group"], .world_entry input[name="automationId"]').each((_, el) => {
const instance = $(el).autocomplete('instance');
if (instance !== undefined) {
$(el).autocomplete('close');
}
});
});
});

View File

@@ -1,4 +1,4 @@
const { TEXTGEN_TYPES } = require('./constants');
const { TEXTGEN_TYPES, OPENROUTER_HEADERS } = require('./constants');
const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
const { getConfigValue } = require('./util');
@@ -19,6 +19,21 @@ function getTogetherAIHeaders() {
}) : {};
}
function getInfermaticAIHeaders() {
const apiKey = readSecret(SECRET_KEYS.INFERMATICAI);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
}) : {};
}
function getOpenRouterHeaders() {
const apiKey = readSecret(SECRET_KEYS.OPENROUTER);
const baseHeaders = { ...OPENROUTER_HEADERS };
return apiKey ? Object.assign(baseHeaders, { 'Authorization': `Bearer ${apiKey}` }) : baseHeaders;
}
function getAphroditeHeaders() {
const apiKey = readSecret(SECRET_KEYS.APHRODITE);
@@ -80,6 +95,12 @@ function setAdditionalHeaders(request, args, server) {
case TEXTGEN_TYPES.OOBA:
headers = getOobaHeaders();
break;
case TEXTGEN_TYPES.INFERMATICAI:
headers = getInfermaticAIHeaders();
break;
case TEXTGEN_TYPES.OPENROUTER:
headers = getOpenRouterHeaders();
break;
default:
headers = server ? getOverrideHeaders((new URL(server))?.host) : {};
break;

View File

@@ -1,41 +1,80 @@
const fs = require('fs');
const encode = require('png-chunks-encode');
const extract = require('png-chunks-extract');
const PNGtext = require('png-chunk-text');
const parse = async (cardUrl, format) => {
/**
* Writes Character metadata to a PNG image buffer.
* @param {Buffer} image PNG image buffer
* @param {string} data Character data to write
* @returns {Buffer} PNG image buffer with metadata
*/
const write = (image, data) => {
const chunks = extract(image);
const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
// Remove all existing tEXt chunks
for (let tEXtChunk of tEXtChunks) {
chunks.splice(chunks.indexOf(tEXtChunk), 1);
}
// Add new chunks before the IEND chunk
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
const newBuffer = Buffer.from(encode(chunks));
return newBuffer;
};
/**
* Reads Character metadata from a PNG image buffer.
* @param {Buffer} image PNG image buffer
* @returns {string} Character data
*/
const read = (image) => {
const chunks = extract(image);
const textChunks = chunks.filter(function (chunk) {
return chunk.name === 'tEXt';
}).map(function (chunk) {
return PNGtext.decode(chunk.data);
});
if (textChunks.length === 0) {
console.error('PNG metadata does not contain any text chunks.');
throw new Error('No PNG metadata.');
}
let index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() == 'chara');
if (index === -1) {
console.error('PNG metadata does not contain any character data.');
throw new Error('No PNG metadata.');
}
return Buffer.from(textChunks[index].text, 'base64').toString('utf8');
};
/**
* Parses a card image and returns the character metadata.
* @param {string} cardUrl Path to the card image
* @param {string} format File format
* @returns {string} Character data
*/
const parse = (cardUrl, format) => {
let fileFormat = format === undefined ? 'png' : format;
switch (fileFormat) {
case 'png': {
const buffer = fs.readFileSync(cardUrl);
const chunks = extract(buffer);
const textChunks = chunks.filter(function (chunk) {
return chunk.name === 'tEXt';
}).map(function (chunk) {
return PNGtext.decode(chunk.data);
});
if (textChunks.length === 0) {
console.error('PNG metadata does not contain any text chunks.');
throw new Error('No PNG metadata.');
}
let index = textChunks.findIndex((chunk) => chunk.keyword.toLowerCase() == 'chara');
if (index === -1) {
console.error('PNG metadata does not contain any character data.');
throw new Error('No PNG metadata.');
}
return Buffer.from(textChunks[index].text, 'base64').toString('utf8');
return read(buffer);
}
default:
break;
}
throw new Error('Unsupported format');
};
module.exports = {
parse: parse,
parse,
write,
read,
};

View File

@@ -176,8 +176,22 @@ const TEXTGEN_TYPES = {
TOGETHERAI: 'togetherai',
LLAMACPP: 'llamacpp',
OLLAMA: 'ollama',
INFERMATICAI: 'infermaticai',
OPENROUTER: 'openrouter',
};
const INFERMATICAI_KEYS = [
'model',
'prompt',
'max_tokens',
'temperature',
'top_p',
'top_k',
'repetition_penalty',
'stream',
'stop',
];
// https://docs.together.ai/reference/completions
const TOGETHERAI_KEYS = [
'model',
@@ -213,6 +227,29 @@ const OLLAMA_KEYS = [
const AVATAR_WIDTH = 400;
const AVATAR_HEIGHT = 600;
const OPENROUTER_HEADERS = {
'HTTP-Referer': 'https://sillytavern.app',
'X-Title': 'SillyTavern',
};
const OPENROUTER_KEYS = [
'max_tokens',
'temperature',
'top_k',
'top_p',
'presence_penalty',
'frequency_penalty',
'repetition_penalty',
'min_p',
'top_a',
'seed',
'logit_bias',
'model',
'stream',
'prompt',
'stop',
];
module.exports = {
DIRECTORIES,
UNSAFE_EXTENSIONS,
@@ -225,4 +262,7 @@ module.exports = {
AVATAR_HEIGHT,
TOGETHERAI_KEYS,
OLLAMA_KEYS,
INFERMATICAI_KEYS,
OPENROUTER_HEADERS,
OPENROUTER_KEYS,
};

View File

@@ -8,7 +8,7 @@ const { DIRECTORIES, UNSAFE_EXTENSIONS } = require('../constants');
const { jsonParser } = require('../express-common');
const { clientRelativePath } = require('../util');
const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm'];
const VALID_CATEGORIES = ['bgm', 'ambient', 'blip', 'live2d', 'vrm', 'character'];
/**
* Validates the input filename for the asset.
@@ -199,6 +199,13 @@ router.post('/download', jsonParser, async (request, response) => {
const fileStream = fs.createWriteStream(destination, { flags: 'wx' });
await finished(res.body.pipe(fileStream));
if (category === 'character') {
response.sendFile(temp_path, { root: process.cwd() }, () => {
fs.rmSync(temp_path);
});
return;
}
// Move into asset place
console.debug('Download finished, moving file from', temp_path, 'to', file_path);
fs.renameSync(temp_path, file_path);

View File

@@ -3,7 +3,7 @@ const fetch = require('node-fetch').default;
const { Readable } = require('stream');
const { jsonParser } = require('../../express-common');
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY } = require('../../constants');
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
const { convertClaudePrompt, convertGooglePrompt, convertTextCompletionPrompt } = require('../prompt-converters');
@@ -419,6 +419,9 @@ async function sendMistralAIRequest(request, response) {
try {
//must send a user role as last message
const messages = Array.isArray(request.body.messages) ? request.body.messages : [];
//large seems to be throwing a 500 error if we don't make the first message a user role, most likely a bug since the other models won't do this
if (request.body.model.includes('large'))
messages[0].role = 'user';
const lastMsg = messages[messages.length - 1];
if (messages.length > 0 && lastMsg && (lastMsg.role === 'system' || lastMsg.role === 'assistant')) {
if (lastMsg.role === 'assistant' && lastMsg.name) {
@@ -513,8 +516,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
api_url = 'https://openrouter.ai/api/v1';
api_key_openai = readSecret(SECRET_KEYS.OPENROUTER);
// OpenRouter needs to pass the referer: https://openrouter.ai/docs
headers = { 'HTTP-Referer': request.headers.referer };
// OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests
headers = { ...OPENROUTER_HEADERS };
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
api_url = new URL(request.body.reverse_proxy || API_MISTRAL).toString();
api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.MISTRALAI);
@@ -700,8 +703,8 @@ router.post('/generate', jsonParser, function (request, response) {
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
apiUrl = 'https://openrouter.ai/api/v1';
apiKey = readSecret(SECRET_KEYS.OPENROUTER);
// OpenRouter needs to pass the referer: https://openrouter.ai/docs
headers = { 'HTTP-Referer': request.headers.referer };
// OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests
headers = { ...OPENROUTER_HEADERS };
bodyParams = { 'transforms': ['middle-out'] };
if (request.body.min_p !== undefined) {

View File

@@ -4,7 +4,7 @@ const _ = require('lodash');
const Readable = require('stream').Readable;
const { jsonParser } = require('../../express-common');
const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS } = require('../../constants');
const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS } = require('../../constants');
const { forwardFetchResponse, trimV1 } = require('../../util');
const { setAdditionalHeaders } = require('../../additional-headers');
@@ -106,6 +106,8 @@ router.post('/status', jsonParser, async function (request, response) {
case TEXTGEN_TYPES.APHRODITE:
case TEXTGEN_TYPES.KOBOLDCPP:
case TEXTGEN_TYPES.LLAMACPP:
case TEXTGEN_TYPES.INFERMATICAI:
case TEXTGEN_TYPES.OPENROUTER:
url += '/v1/models';
break;
case TEXTGEN_TYPES.MANCER:
@@ -208,6 +210,7 @@ router.post('/generate', jsonParser, async function (request, response) {
request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
}
const apiType = request.body.api_type;
const baseUrl = request.body.api_server;
console.log(request.body);
@@ -232,6 +235,7 @@ router.post('/generate', jsonParser, async function (request, response) {
case TEXTGEN_TYPES.TABBY:
case TEXTGEN_TYPES.KOBOLDCPP:
case TEXTGEN_TYPES.TOGETHERAI:
case TEXTGEN_TYPES.INFERMATICAI:
url += '/v1/completions';
break;
case TEXTGEN_TYPES.MANCER:
@@ -243,6 +247,9 @@ router.post('/generate', jsonParser, async function (request, response) {
case TEXTGEN_TYPES.OLLAMA:
url += '/api/generate';
break;
case TEXTGEN_TYPES.OPENROUTER:
url += '/v1/chat/completions';
break;
}
}
@@ -261,11 +268,22 @@ router.post('/generate', jsonParser, async function (request, response) {
args.body = JSON.stringify(request.body);
}
if (request.body.api_type === TEXTGEN_TYPES.INFERMATICAI) {
request.body = _.pickBy(request.body, (_, key) => INFERMATICAI_KEYS.includes(key));
args.body = JSON.stringify(request.body);
}
if (request.body.api_type === TEXTGEN_TYPES.OPENROUTER) {
request.body = _.pickBy(request.body, (_, key) => OPENROUTER_KEYS.includes(key));
args.body = JSON.stringify(request.body);
}
if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
args.body = JSON.stringify({
model: request.body.model,
prompt: request.body.prompt,
stream: request.body.stream ?? false,
keep_alive: -1,
raw: true,
options: _.pickBy(request.body, (_, key) => OLLAMA_KEYS.includes(key)),
});
@@ -292,6 +310,11 @@ router.post('/generate', jsonParser, async function (request, response) {
data['choices'] = [{ text }];
}
// Map InfermaticAI response to OAI completions format
if (apiType === TEXTGEN_TYPES.INFERMATICAI) {
data['choices'] = (data?.choices || []).map(choice => ({ text: choice.message.content }));
}
return response.send(data);
} else {
const text = await completionsReply.text();

View File

@@ -7,9 +7,6 @@ const writeFileAtomicSync = require('write-file-atomic').sync;
const yaml = require('yaml');
const _ = require('lodash');
const encode = require('png-chunks-encode');
const extract = require('png-chunks-extract');
const PNGtext = require('png-chunk-text');
const jimp = require('jimp');
const { DIRECTORIES, UPLOADS_PATH, AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
@@ -33,7 +30,7 @@ const characterDataCache = new Map();
* @param {string} input_format - 'png'
* @returns {Promise<string | undefined>} - Character card data
*/
async function charaRead(img_url, input_format) {
async function charaRead(img_url, input_format = 'png') {
const stat = fs.statSync(img_url);
const cacheKey = `${img_url}-${stat.mtimeMs}`;
if (characterDataCache.has(cacheKey)) {
@@ -59,22 +56,12 @@ async function charaWrite(img_url, data, target_img, response = undefined, mes =
}
}
// Read the image, resize, and save it as a PNG into the buffer
const image = await tryReadImage(img_url, crop);
const inputImage = await tryReadImage(img_url, crop);
// Get the chunks
const chunks = extract(image);
const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt');
const outputImage = characterCardParser.write(inputImage, data);
// Remove all existing tEXt chunks
for (let tEXtChunk of tEXtChunks) {
chunks.splice(chunks.indexOf(tEXtChunk), 1);
}
// Add new chunks before the IEND chunk
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
//chunks.splice(-1, 0, text.encode('lorem', 'ipsum'));
writeFileAtomicSync(DIRECTORIES.characters + target_img + '.png', Buffer.from(encode(chunks)));
writeFileAtomicSync(DIRECTORIES.characters + target_img + '.png', outputImage);
if (response !== undefined) response.send(mes);
return true;
} catch (err) {
@@ -152,13 +139,13 @@ const processCharacter = async (item, i) => {
const img_data = await charaRead(DIRECTORIES.characters + item);
if (img_data === undefined) throw new Error('Failed to read character file');
let jsonObject = getCharaCardV2(JSON.parse(img_data));
let jsonObject = getCharaCardV2(JSON.parse(img_data), false);
jsonObject.avatar = item;
characters[i] = jsonObject;
characters[i]['json_data'] = img_data;
const charStat = fs.statSync(path.join(DIRECTORIES.characters, item));
characters[i]['date_added'] = charStat.birthtimeMs;
characters[i]['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.birthtimeMs);
characters[i]['date_added'] = charStat.ctimeMs;
characters[i]['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.ctimeMs);
const char_dir = path.join(DIRECTORIES.chats, item.replace('.png', ''));
const { chatSize, dateLastChat } = calculateChatSize(char_dir);
@@ -183,15 +170,30 @@ const processCharacter = async (item, i) => {
}
};
function getCharaCardV2(jsonObject) {
/**
* Convert a character object to Spec V2 format.
* @param {object} jsonObject Character object
* @param {boolean} hoistDate Will set the chat and create_date fields to the current date if they are missing
* @returns {object} Character object in Spec V2 format
*/
function getCharaCardV2(jsonObject, hoistDate = true) {
if (jsonObject.spec === undefined) {
jsonObject = convertToV2(jsonObject);
if (hoistDate && !jsonObject.create_date) {
jsonObject.create_date = humanizedISO8601DateTime();
}
} else {
jsonObject = readFromV2(jsonObject);
}
return jsonObject;
}
/**
* Convert a character object to Spec V2 format.
* @param {object} char Character object
* @returns {object} Character object in Spec V2 format
*/
function convertToV2(char) {
// Simulate incoming data from frontend form
const result = charaFormatData({
@@ -212,7 +214,8 @@ function convertToV2(char) {
});
result.chat = char.chat ?? humanizedISO8601DateTime();
result.create_date = char.create_date ?? humanizedISO8601DateTime();
result.create_date = char.create_date;
return result;
}
@@ -392,6 +395,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
scan_depth: entry.scanDepth ?? null,
match_whole_words: entry.matchWholeWords ?? null,
case_sensitive: entry.caseSensitive ?? null,
automation_id: entry.automationId ?? '',
},
};
@@ -796,6 +800,17 @@ function getPngName(file) {
return file;
}
/**
* Gets the preserved name for the uploaded file if the request is valid.
* @param {import("express").Request} request - Express request object
* @returns {string | undefined} - The preserved name if the request is valid, otherwise undefined
*/
function getPreservedName(request) {
return request.body.file_type === 'png' && request.body.preserve_file_name === 'true' && request.file?.originalname
? path.parse(request.file.originalname).name
: undefined;
}
router.post('/import', urlencodedParser, async function (request, response) {
if (!request.body || !request.file) return response.sendStatus(400);
@@ -803,6 +818,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
let filedata = request.file;
let uploadPath = path.join(UPLOADS_PATH, filedata.filename);
let format = request.body.file_type;
const preservedFileName = getPreservedName(request);
if (format == 'yaml' || format == 'yml') {
try {
@@ -894,7 +910,7 @@ router.post('/import', urlencodedParser, async function (request, response) {
let jsonData = JSON.parse(img_data);
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
png_name = getPngName(jsonData.name);
png_name = preservedFileName || getPngName(jsonData.name);
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');

View File

@@ -10,6 +10,7 @@ const contentLogPath = path.join(contentDirectory, 'content.log');
const contentIndexPath = path.join(contentDirectory, 'index.json');
const { DIRECTORIES } = require('../constants');
const presetFolders = [DIRECTORIES.koboldAI_Settings, DIRECTORIES.openAI_Settings, DIRECTORIES.novelAI_Settings, DIRECTORIES.textGen_Settings];
const characterCardParser = require('../character-card-parser.js');
/**
* Gets the default presets from the content directory.
@@ -219,6 +220,56 @@ async function downloadChubCharacter(id) {
return { buffer, fileName, fileType };
}
/**
* Downloads a character card from the Pygsite.
* @param {string} id UUID of the character
* @returns {Promise<{buffer: Buffer, fileName: string, fileType: string}>}
*/
async function downloadPygmalionCharacter(id) {
const result = await fetch(`https://server.pygmalion.chat/api/export/character/${id}/v2`);
if (!result.ok) {
const text = await result.text();
console.log('Pygsite returned error', result.status, text);
throw new Error('Failed to download character');
}
const jsonData = await result.json();
const characterData = jsonData?.character;
if (!characterData || typeof characterData !== 'object') {
console.error('Pygsite returned invalid character data', jsonData);
throw new Error('Failed to download character');
}
try {
const avatarUrl = characterData?.data?.avatar;
if (!avatarUrl) {
console.error('Pygsite character does not have an avatar', characterData);
throw new Error('Failed to download avatar');
}
const avatarResult = await fetch(avatarUrl);
const avatarBuffer = await avatarResult.buffer();
const cardBuffer = characterCardParser.write(avatarBuffer, JSON.stringify(characterData));
return {
buffer: cardBuffer,
fileName: `${sanitize(id)}.png`,
fileType: 'image/png',
};
} catch (e) {
console.error('Failed to download avatar, using JSON instead', e);
return {
buffer: Buffer.from(JSON.stringify(jsonData)),
fileName: `${sanitize(id)}.json`,
fileType: 'application/json',
};
}
}
/**
*
* @param {String} str
@@ -294,7 +345,7 @@ async function downloadJannyCharacter(uuid) {
* @param {String} url
* @returns {String | null } UUID of the character
*/
function parseJannyUrl(url) {
function getUuidFromUrl(url) {
// Extract UUID from URL
const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;
const matches = url.match(uuidRegex);
@@ -317,8 +368,18 @@ router.post('/import', jsonParser, async (request, response) => {
let type;
const isJannnyContent = url.includes('janitorai');
if (isJannnyContent) {
const uuid = parseJannyUrl(url);
const isPygmalionContent = url.includes('pygmalion.chat');
if (isPygmalionContent) {
const uuid = getUuidFromUrl(url);
if (!uuid) {
return response.sendStatus(404);
}
type = 'character';
result = await downloadPygmalionCharacter(uuid);
} else if (isJannnyContent) {
const uuid = getUuidFromUrl(url);
if (!uuid) {
return response.sendStatus(404);
}

View File

@@ -30,8 +30,17 @@ const SECRET_KEYS = {
MISTRALAI: 'api_key_mistralai',
CUSTOM: 'api_key_custom',
OOBA: 'api_key_ooba',
INFERMATICAI: 'api_key_infermaticai',
};
// These are the keys that are safe to expose, even if allowKeysExposure is false
const EXPORTABLE_KEYS = [
SECRET_KEYS.LIBRE_URL,
SECRET_KEYS.LINGVA_URL,
SECRET_KEYS.ONERING_URL,
SECRET_KEYS.DEEPLX_URL,
];
/**
* Writes a secret to the secrets file
* @param {string} key Secret key
@@ -212,14 +221,13 @@ router.post('/view', jsonParser, async (_, response) => {
router.post('/find', jsonParser, (request, response) => {
const allowKeysExposure = getConfigValue('allowKeysExposure', false);
const key = request.body.key;
if (!allowKeysExposure) {
if (!allowKeysExposure && !EXPORTABLE_KEYS.includes(key)) {
console.error('Cannot fetch secrets unless allowKeysExposure in config.yaml is set to true');
return response.sendStatus(403);
}
const key = request.body.key;
try {
const secret = readSecret(key);

View File

@@ -4,6 +4,9 @@ const express = require('express');
const sanitize = require('sanitize-filename');
const { jsonParser } = require('../express-common');
// Don't forget to add new sources to the SOURCES array
const SOURCES = ['transformers', 'mistral', 'openai', 'extras', 'palm'];
/**
* Gets the vector for the given text from the given source.
* @param {string} source - The source of the vector
@@ -261,8 +264,7 @@ router.post('/purge', jsonParser, async (req, res) => {
const collectionId = String(req.body.collectionId);
const sources = ['transformers', 'openai', 'palm'];
for (const source of sources) {
for (const source of SOURCES) {
const index = await getIndex(collectionId, source, false);
const exists = await index.isIndexCreated();

View File

@@ -365,7 +365,7 @@ function getImages(path) {
/**
* Pipe a fetch() response to an Express.js Response, including status code.
* @param {import('node-fetch').Response} from The Fetch API response to pipe from.
* @param {Express.Response} to The Express response to pipe to.
* @param {import('express').Response} to The Express response to pipe to.
*/
function forwardFetchResponse(from, to) {
let statusCode = from.status;
@@ -399,6 +399,64 @@ function forwardFetchResponse(from, to) {
});
}
/**
* Makes an HTTP/2 request to the specified endpoint.
*
* @deprecated Use `node-fetch` if possible.
* @param {string} endpoint URL to make the request to
* @param {string} method HTTP method to use
* @param {string} body Request body
* @param {object} headers Request headers
* @returns {Promise<string>} Response body
*/
function makeHttp2Request(endpoint, method, body, headers) {
return new Promise((resolve, reject) => {
try {
const http2 = require('http2');
const url = new URL(endpoint);
const client = http2.connect(url.origin);
const req = client.request({
':method': method,
':path': url.pathname,
...headers,
});
req.setEncoding('utf8');
req.on('response', (headers) => {
const status = Number(headers[':status']);
if (status < 200 || status >= 300) {
reject(new Error(`Request failed with status ${status}`));
}
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
console.log(data);
resolve(data);
});
});
req.on('error', (err) => {
reject(err);
});
if (body) {
req.write(body);
}
req.end();
} catch (e) {
reject(e);
}
});
}
/**
* Adds YAML-serialized object to the object.
* @param {object} obj Object
@@ -547,4 +605,5 @@ module.exports = {
excludeKeysByYaml,
trimV1,
Cache,
makeHttp2Request,
};