mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Compare commits
125 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6f5dbc2a52 | ||
|
0fcb176408 | ||
|
31f39e30c2 | ||
|
5a236fbccb | ||
|
2a4b8ac438 | ||
|
7885f19e86 | ||
|
1898192d37 | ||
|
29c4334c46 | ||
|
73886c9fff | ||
|
149a65cf62 | ||
|
617ae7d02c | ||
|
c58d0b2b94 | ||
|
e86fd08d0f | ||
|
f962ad5c02 | ||
|
9e5505a7d4 | ||
|
fc289126fa | ||
|
d5bf9fc28c | ||
|
d140b8d5be | ||
|
3cedf64f66 | ||
|
0e357c191b | ||
|
3441667336 | ||
|
7b8ac8f4c4 | ||
|
16833fc238 | ||
|
8848818d67 | ||
|
299bd9d563 | ||
|
13aebc623a | ||
|
eaadfea639 | ||
|
9287ff18de | ||
|
dab9bbb514 | ||
|
445cbda02f | ||
|
9eba076ae4 | ||
|
936fbac6c5 | ||
|
737a0bd3ae | ||
|
9b34ac1bde | ||
|
cb536a7611 | ||
|
82c5042bad | ||
|
4baefeba68 | ||
|
344b9eedbc | ||
|
f82740a238 | ||
|
bc2010a762 | ||
|
eb89337f51 | ||
|
c9f0d61f19 | ||
|
f569424f3e | ||
|
beb5e470a2 | ||
|
ece3b2a7c1 | ||
|
06c3ea7c51 | ||
|
0ccdfe4bb7 | ||
|
40aa971d11 | ||
|
fb6fa54c7f | ||
|
fcf171931a | ||
|
92af4137a9 | ||
|
711fd0517f | ||
|
d31195a704 | ||
|
10fb69f36a | ||
|
d353fa58d0 | ||
|
96f1ce1fce | ||
|
0c1cf9ff2e | ||
|
7c12c836f2 | ||
|
48b9eb8542 | ||
|
f43e686301 | ||
|
f0141b4dd1 | ||
|
1e7c2820da | ||
|
095cd873de | ||
|
8ba9b5c38b | ||
|
8e66a14e37 | ||
|
79ba026486 | ||
|
cec0698400 | ||
|
f3971686ea | ||
|
32ee58e5e6 | ||
|
0d8858285f | ||
|
061b7c6922 | ||
|
a5ee46cb2a | ||
|
550d8483cc | ||
|
2e00a1baaf | ||
|
030806bf1e | ||
|
e55d903613 | ||
|
90d5fbc182 | ||
|
3c2113a6e7 | ||
|
0391179c3c | ||
|
e4a48cd28f | ||
|
c20a9fb5f5 | ||
|
02a2e26e2b | ||
|
ec826450dc | ||
|
29b971a986 | ||
|
b072057594 | ||
|
c06fe6abfc | ||
|
a8cd6c9fe7 | ||
|
7fbef32869 | ||
|
106cdf3aed | ||
|
c6c73fedad | ||
|
c8b0030f6e | ||
|
8075e4cd1e | ||
|
b5887960b6 | ||
|
187ecc2046 | ||
|
0da0d490c7 | ||
|
6ab918605e | ||
|
e3102bb26f | ||
|
dbfe7ae7c6 | ||
|
f12993ffb7 | ||
|
8d4c4c1945 | ||
|
604f9732be | ||
|
90963f6437 | ||
|
b8387df15e | ||
|
9d713825c2 | ||
|
5f6bc49aa6 | ||
|
72256110a7 | ||
|
85df989193 | ||
|
0480488127 | ||
|
fe080cfec3 | ||
|
7bcb6f1ee1 | ||
|
4d958b9df7 | ||
|
354c52d997 | ||
|
33d93b9761 | ||
|
2d152d2705 | ||
|
f183f55c74 | ||
|
6e10c43c63 | ||
|
9fed7ed742 | ||
|
70deb11d27 | ||
|
818029288e | ||
|
cbea5bf996 | ||
|
2065f95edc | ||
|
87668f5962 | ||
|
df97f5364b | ||
|
cab6f90519 | ||
|
f29f934c6b |
42
.github/readme-ja_jp.md
vendored
42
.github/readme-ja_jp.md
vendored
@@ -2,15 +2,15 @@
|
||||
|
||||

|
||||
|
||||
モバイルフレンドリーなレイアウト、マルチAPI(KoboldAI/CPP、Horde、NovelAI、Ooba、OpenAI、OpenRouter、Claude、Scale)、VN ライクな Waifu モード、Stable Diffusion、TTS、WorldInfo(伝承本)、カスタマイズ可能な UI、自動翻訳、あなたにとって必要とする以上のプロンプトオプション+サードパーティの拡張機能をインストールする機能。
|
||||
モバイルデバイスにも適したレイアウト・マルチAPI(KoboldAI/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. 何の脈絡もなくバグ報告を送る
|
||||
|
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -4,8 +4,8 @@ name: Create Docker Image on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
# Only runs on full releases not pre releases
|
||||
types: [released]
|
||||
# Allow pre-releases
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
# This should allow creation of docker images even in forked repositories
|
||||
|
@@ -34,7 +34,8 @@ RUN \
|
||||
rm -f "config.yaml" "public/settings.json" || true && \
|
||||
ln -s "./config/config.yaml" "config.yaml" || true && \
|
||||
ln -s "../config/settings.json" "public/settings.json" || true && \
|
||||
mkdir "config" || true
|
||||
mkdir "config" || true && \
|
||||
mkdir -p "public/user" || true
|
||||
|
||||
# Cleanup unnecessary files
|
||||
RUN \
|
||||
|
@@ -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
12
package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
350
public/i18n.json
350
public/i18n.json
@@ -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)": "Sentencepiece(LLaMA)",
|
||||
"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
158
public/script.js
158
public/script.js
@@ -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 });
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
||||
|
@@ -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));
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
|
@@ -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(),
|
||||
|
@@ -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>
|
||||
|
@@ -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'));
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
});
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -313,6 +313,8 @@ class PresetManager {
|
||||
'type',
|
||||
'custom_model',
|
||||
'bypass_status_check',
|
||||
'infermaticai_model',
|
||||
'openrouter_model',
|
||||
];
|
||||
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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 '';
|
||||
|
@@ -16,6 +16,8 @@
|
||||
<li><tt>{{mesExamplesRaw}}</tt> – unformatted Dialogue Examples <b>(only for Story String)</b></li>
|
||||
<li><tt>{{user}}</tt> – your current Persona username</li>
|
||||
<li><tt>{{char}}</tt> – the Character's name</li>
|
||||
<li><tt>{{group}}</tt> – a comma-separated list of group member names or the character name in solo chats. Alias: {{charIfNotGroup}}</li>
|
||||
<li><tt>{{model}}</tt> – a text generation model name for the currently selected API. <b>Can be inaccurate!</b></li>
|
||||
<li><tt>{{lastMessage}}</tt> - the text of the latest chat message.</li>
|
||||
<li><tt>{{lastMessageId}}</tt> – index # of the latest chat message. Useful for slash command batching.</li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> - the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</li>
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
|
@@ -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.');
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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();
|
||||
|
61
src/util.js
61
src/util.js
@@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user