Merge remote-tracking branch 'origin/staging' into llamacpp-probs

This commit is contained in:
Deciare 2024-02-23 17:45:54 -05:00
commit 936fbac6c5
26 changed files with 1262 additions and 460 deletions

0
.nomedia Normal file
View File

View File

@ -47,6 +47,20 @@
"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"
],
"mirostat_mode": 0,
"mirostat_tau": 5,
"mirostat_eta": 0.1,

8
package-lock.json generated
View File

@ -12,7 +12,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",
@ -78,9 +78,9 @@
"integrity": "sha512-KlmTftToTtmb6aLVdne4NluS+POWputPF5J8v25UN/EQS+K9vahWEIe1NPRSFqBQclObkqHaj7JOnFrmnSm5MA=="
},
"node_modules/@dqbd/tiktoken": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.7.tgz",
"integrity": "sha512-bhR5k5W+8GLzysjk8zTMVygQZsgvf7W1F0IlL4ZQ5ugjo5rCyiwGM5d8DYriXspytfu98tv59niang3/T+FoDw=="
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.13.tgz",
"integrity": "sha512-941kjlHjfI97l6NuH/AwuXV4mHuVnRooDcHNSlzi98hz+4ug3wT4gJcWjSwSZHqeGAEn90lC9sFD+8a9d5Jvxg=="
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",

View File

@ -2,7 +2,7 @@
"dependencies": {
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.2",
"@dqbd/tiktoken": "^1.0.13",
"bing-translate-api": "^2.9.1",
"body-parser": "^1.20.2",
"command-exists": "^1.2.9",

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1603,15 +1603,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 +2169,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 +2197,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 +2270,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());
@ -2857,7 +2887,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');
@ -5899,10 +5929,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
@ -7743,7 +7773,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 +7791,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 +7815,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 +7873,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 +8042,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 +8493,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 +8505,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();
}
}
@ -8647,6 +8691,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 +8733,7 @@ jQuery(async function () {
}
else {
is_send_press = true;
Generate('regenerate');
Generate('regenerate', buildOrFillAdditionalArgs());
}
}
}
@ -8690,14 +8741,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 +9885,7 @@ jQuery(async function () {
<li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
<li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>JanitorAI character (direct link or id)<br>Example: <tt>https://janitorai.com/characters/ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li>Pygmalion.chat character (link)<br>Example: <tt>https://pygmalion.chat/character/a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li>More coming soon...</li>
<ul>`;
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
@ -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 });
}
@ -3735,9 +3750,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 +4215,7 @@ $(document).ready(async function () {
oai_settings.chat_completion_source = String($(this).find(':selected').val());
toggleChatCompletionForms();
saveSettingsDebounced();
if (main_api == 'openai') {
reconnectOpenAi();
}
reconnectOpenAi();
eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source);
});

View File

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

View File

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

View File

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

View File

@ -34,6 +34,20 @@ export const textgen_types = {
};
const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP } = textgen_types;
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?
@ -96,6 +110,7 @@ const settings = {
negative_prompt: '',
grammar_string: '',
banned_tokens: '',
sampler_priority: OOBA_DEFAULT_ORDER,
//n_aphrodite: 1,
//best_of_aphrodite: 1,
ignore_eos_token_aphrodite: false,
@ -170,6 +185,7 @@ const setting_names = [
//'log_probs_aphrodite',
//'prompt_log_probs_aphrodite'
'sampler_order',
'sampler_priority',
'n',
'logit_bias',
'custom_model',
@ -422,7 +438,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 +449,16 @@ function sortItemsByOrder(orderArray) {
}
}
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 +475,27 @@ jQuery(function () {
$('#koboldcpp_default_order').on('click', function () {
settings.sampler_order = KOBOLDCPP_ORDER;
sortItemsByOrder(settings.sampler_order);
sortKoboldItemsByOrder(settings.sampler_order);
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,6 +582,7 @@ 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)) {
@ -615,11 +662,18 @@ 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 ('logit_bias' === setting) {
settings.logit_bias = Array.isArray(value) ? value : [];
return;
@ -835,6 +889,7 @@ 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,
'stopping_strings': getStoppingStrings(isImpersonate, isContinue),
'stop': getStoppingStrings(isImpersonate, isContinue),
'truncation_length': max_context,

View File

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

View File

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

View File

@ -513,8 +513,11 @@ 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 = {
'HTTP-Referer': 'https://sillytavern.app',
'X-Title': 'SillyTavern',
};
} 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,11 @@ 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 = {
'HTTP-Referer': 'https://sillytavern.app',
'X-Title': 'SillyTavern',
};
bodyParams = { 'transforms': ['middle-out'] };
if (request.body.min_p !== undefined) {

View File

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

View File

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

View File

@ -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,
};