Merge branch 'dev' into ouoertheo/objective3

This commit is contained in:
ouoertheo
2023-06-06 00:07:11 -05:00
committed by GitHub
71 changed files with 3502 additions and 1053 deletions

View File

@ -1,7 +1,7 @@
#!/bin/sh
# Initialize missing user files
IFS="," RESOURCES="characters,groups,group chats,chats,User Avatars,settings.json"
IFS="," RESOURCES="characters,groups,group chats,chats,User Avatars,worlds,settings.json"
for R in $RESOURCES; do
if [ ! -e "config/$R" ]; then
echo "Resource not found, copying from defaults: $R"

44
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.6.5",
"version": "1.6.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.6.5",
"version": "1.6.6",
"license": "AGPL-3.0",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
@ -838,6 +838,20 @@
"version": "1.1.2",
"license": "MIT"
},
"node_modules/bufferutil": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz",
"integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"dependencies": {
@ -2104,6 +2118,18 @@
}
}
},
"node_modules/node-gyp-build": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
"integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-rest-client": {
"version": "3.1.1",
"license": "MIT",
@ -3207,6 +3233,20 @@
"node": ">= 0.8"
}
},
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/utf8-byte-length": {
"version": "1.0.4",
"license": "WTFPL"

View File

@ -48,7 +48,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.6.5",
"version": "1.6.6",
"scripts": {
"start": "node server.js",
"pkg": "pkg --compress Gzip --no-bytecode --public ."
@ -69,7 +69,7 @@
],
"assets": [
"node_modules/**/*",
"poe_graphql/**/*"
"src/poe_graphql/**/*"
],
"outputPath": "dist",
"scripts": [

View File

@ -1,4 +1,4 @@
const poe = require('./poe-client');
const poe = require('./src/poe-client');
async function test() {
const client = new poe.Client();

583
public/i18n.json Normal file
View File

@ -0,0 +1,583 @@
{
"lang": ["en", "zh-cn"],
"en": {
"clickslidertips": "Click slider numbers to input manually.",
"kobldpresets": "Kobold Presets",
"guikoboldaisettings":"GUI KoboldAI Settings",
"novelaipreserts":"NovelAI Presets",
"default":"Default",
"openaipresets":"OpenAI Presets",
"text gen webio(ooba) presets":"Text Gen WebUI (ooba) presets",
"poe.com api settings":"Poe.com API Settings",
"response legth(tokens)":" Response Length (tokens)",
"context size(tokens)":" Context Size (tokens)",
"unlocked":" Unlocked",
"only select modls support context sizes greater than 2048 tokens. proceed only is you know you're doing":"Only select models support context sizes greater than 2048 tokens.Proceed only if you know what you're doing.",
"rep.pen range":"Rep. Pen. Range",
"temperature":"Temperature",
"rep.pen":"Rep. Pen.",
"Encoder Rep. Pen.":"Encoder Rep. Pen.",
"No Repeat Ngram Size":"No Repeat Ngram Size",
"Min Length":"Min Length",
"OpenAI Reverse Proxy":"OpenAI Reverse Proxy",
"Alternative server URL (leave empty to use the default value).":"Alternative server URL (leave empty to use the default value).",
"Remove your real OAI API Key from the API panel BEFORE typing anything into this box":"Remove your real OAI API Key from the API panel BEFORE typing anything into this box.",
"We cannot provide support for problems encountered while using an unofficial OpenAI proxy":"We cannot provide support for problems encountered while using an unofficial OpenAI proxy.",
"Legacy Streaming Processing":"Legacy Streaming Processing",
"Enable this if the streaming doesn't work with your proxy":"Enable this if the streaming doesn't work with your proxy",
"Context Size (tokens)":"Context Size (tokens)",
"Max Response Length (tokens)":"Max Response Length (tokens)",
"Temperature":" Temperature",
"Frequency Penalty":"Frequency Penalty",
"Presence Penalty":"Presence Penalty",
"Top-p":"Top-p",
"Display bot response text chunks as they are generated":"Display bot response text chunks as they are generated.",
"Auto-purge API context (save JB)":"Auto-purge API context (save JB)",
"Delete non-JB messages from Poe context before sending a new prompt. Prevents auto-jailbreak message from being pushed out of context":"Delete non-JB messages from Poe context before sending a new prompt. Prevents auto-jailbreak message from being pushed out of context",
"Auto-jailbreak":"Auto-jailbreak",
"Send the jailbreak message before first generation after page refresh.":"Send the jailbreak message before first generation after page refresh.",
"Send character note":"Send character note",
"Sent with every prompt to modify bot responses.":"Sent with every prompt to modify bot responses.",
"Top A":"Top A",
"Typical Sampling":"Typical Sampling",
"Tail Free Sampling":"Tail Free Sampling",
"Rep. Pen. Slope":"Rep. Pen. Slope",
"Single-line mode":"Single-line mode",
"Top K":"Top K",
"Top P":"Top P",
"select":"select",
"Typical P":"Typical P",
"Do Sample":"Do Sample",
"Add BOS Token":"Add BOS Token",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.":"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.",
"Ban EOS Token":"Ban EOS Token",
"Ban the eos_token. This forces the model to never end the generation prematurely":"Ban the eos_token. This forces the model to never end the generation prematurely",
"Skip Special Tokens":"Skip Special Tokens",
"Beam search":"Beam search",
"Number of Beams":"Number of Beams",
"Length Penalty":"Length Penalty",
"Early Stopping":"Early Stopping",
"Contrastive search":"Contrastive search",
"Penalty Alpha":"Penalty Alpha",
"Seed":"Seed",
"Inserts jailbreak as a last system message.":"Inserts jailbreak as a last system message.",
"This tells the AI to ignore its usual content restrictions.":"This tells the AI to ignore its usual content restrictions.",
"NSFW Encouraged":"NSFW Encouraged",
"Tell the AI that NSFW is allowed.":"Tell the AI that NSFW is allowed.",
"NSFW Prioritized":"NSFW Prioritized",
"NSFW prompt text goes first in the prompt to emphasize its effect.":"NSFW prompt text goes first in the prompt to emphasize its effect.",
"Streaming":"Streaming",
"Display the response bit by bit as it is generated.":"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.":"When this is off, responses will be displayed all at once when they are complete.",
"Enhance Definitions":"Enhance Definitions",
"Use OAI knowledge base to enhance definitions for public figures and known fictional characters":"Use OAI knowledge base to enhance definitions for public figures and known fictional characters",
"Wrap in Quotes":"Wrap in Quotes",
"Wrap entire user message in quotes before sending.":"Wrap entire user message in quotes before sending.",
"Leave off if you use quotes manually for speech.":"Leave off if you use quotes manually for speech.",
"Main prompt":"Main prompt",
"The main prompt used to set the model behavior":"The main prompt used to set the model behavior",
"NSFW prompt":"NSFW prompt",
"Prompt that is used when the NSFW toggle is on":"Prompt that is used when the NSFW toggle is on",
"Jailbreak prompt":"Jailbreak prompt",
"Prompt that is used when the Jailbreak toggle is on":"Prompt that is used when the Jailbreak toggle is on",
"Impersonation prompt":"Impersonation prompt",
"Prompt that is used for Impersonation function":"Prompt that is used for Impersonation function",
"Logit Bias":"Logit Bias",
"Helps to ban or reenforce the usage of certain words":"Helps to ban or reenforce the usage of certain words",
"View / Edit bias preset":"View / Edit bias preset",
"Add bias entry":"Add bias entry",
"Jailbreak activation message":"Jailbreak activation message",
"Message to send when auto-jailbreak is on.":"Message to send when auto-jailbreak is on.",
"Jailbreak confirmation reply":"Jailbreak confirmation reply",
"Bot must send this back to confirm jailbreak":"Bot must send this back to confirm jailbreak",
"Character Note":"Character Note",
"Influences bot behavior in its responses":"Influences bot behavior in its responses",
"API":"API",
"KoboldAI":"KoboldAI",
"Poe":"Poe",
"Use Horde":"Use Horde",
"API url":"API url",
"Register a Horde account for faster queue times":"Register a Horde account for faster queue times",
"Learn how to contribute your idle GPU cycles to the Hord":"Learn how to contribute your idle GPU cycles to the Hord",
"Adjust context size to worker capabilities":"Adjust context size to worker capabilities",
"Adjust response length to worker capabilities":"Adjust response length to worker capabilities",
"API key":"API key",
"Register":"Register",
"For privacy reasons":"For privacy reasons, your API key will be hidden after you reload the page",
"Model":"Model",
"Hold Control / Command key to select multiple models.":"Hold Control / Command key to select multiple models.",
"Horde models not loaded":"Horde models not loaded",
"Not connected":"Not connected",
"Novel API key":"Novel API key",
"Follow":"Follow",
"these directions":" these directions ",
"to get your NovelAI API key.":"to get your NovelAI API key.",
"Enter it in the box below":"Enter it in the box below:",
"Novel AI Model":"Novel AI Model",
"Euterpe":"Euterpe",
"Krake":"Krake",
"No connection":"No connection...",
"oobabooga/text-generation-webui":"oobabooga/text-generation-webui",
"Make sure you run it with":" Make sure you run it with --api flag",
"Blocking API url":"Blocking API url",
"Streaming API url":"Streaming API url",
"to get your OpenAI API key.":"to get your OpenAI API key.",
"OpenAI Model":"OpenAI Model",
"View API Usage Metrics":"View API Usage Metrics",
"Poe.com Unofficial API":"Poe.com Unofficial API",
"to get your 'p-b cookie'":"to get your 'p-b cookie'",
"Bot":"Bot",
"Connect to the API":"Connect to the API",
"Auto-connect to Last Server":"Auto-connect to Last Server",
"View hidden API keys":"View hidden API keys",
"Advanced Formatting":"Advanced Formatting",
"AutoFormat Overrides":"AutoFormat Overrides",
"Disable description formatting":"Disable description formatting",
"Disable personality formatting":"Disable personality formatting",
"Disable scenario formatting":"Disable scenario formatting",
"Disable example chats formatting":"Disable example chats formatting",
"Disable chat start formatting":"Disable chat start formatting",
"Custom Chat Separator":"Custom Chat Separator",
"Instruct mode":"Instruct mode",
"Enabled":"Enabled",
"Wrap Sequences with Newline":"Wrap Sequences with Newline",
"Include Names":"Include Names",
"System Prompt":"System Prompt",
"Input Sequence":"Input Sequence",
"Output Sequence":"Output Sequence",
"System Sequence":"System Sequence",
"Stop Sequence":"Stop Sequence",
"Context Formatting":"Context Formatting",
"Tokenizer":"Tokenizer",
"None / Estimated":"None / Estimated",
"Sentencepiece (LLaMA)":"Sentencepiece (LLaMA)",
"Token Padding":"Token Padding",
"Always add character's name to prompt":"Always add character's name to prompt",
"Keep Example Messages in Prompt":"Keep Example Messages in Prompt",
"Remove Empty New Lines from Output":"Remove Empty New Lines from Output",
"Pygmalion Formatting":"Pygmalion Formatting",
"Disabled for all models":"Disabled for all models",
"Automatic (based on model name)":"Automatic (based on model name)",
"Enabled for all models":"Enabled for all models",
"Multigen":"Multigen",
"First chunk (tokens)":"First chunk (tokens)",
"Next chunks (tokens)":"Next chunks (tokens)",
"Anchors Order":"Anchors Order",
"Character then Style":"Character then Style",
"Style then Character":"Style then Character",
"Character Anchor":"Character Anchor",
"Style Anchor":"Style Anchor",
"World Info":"World Info",
"Scan Depth":"Scan Depth",
"depth":"depth",
"Token Budget":"Token Budget",
"budget":"budget",
"Recursive scanning":"Recursive scanning",
"Soft Prompt":"Soft Prompt",
"About soft prompts":"About soft prompts",
"None":"None",
"User Settings":"User Settings",
"UI Customization":"UI Customization",
"Avatar Style":"Avatar Style:",
"Circle":"Circle",
"Rectangle":"Rectangle",
"Chat Style":"Chat Style:",
"Default":"Default",
"Bubbles":"Bubbles",
"Chat Width (PC)":"Chat Width (PC):",
"No Blur Effect":"No Blur Effect",
"No Text Shadows":"No Text Shadows",
"Waifu Mode":" ♡ Waifu Mode ♡",
"Message Timer":"Message Timer",
"Characters Hotswap":"Characters Hotswap",
"Movable UI Panels":"Movable UI Panels",
"Reset Panels":"Reset Panels",
"UI Colors":"UI Colors",
"Main Text":"Main Text",
"Italics Text":"Italics Text",
"Quote Text":"Quote Text",
"Shadow Color":"Shadow Color",
"FastUI BG":"FastUI BG",
"Blur Tint":"Blur Tint",
"Font Scale":"Font Scale",
"Blur Strength":"Blur Strength",
"Text Shadow Width":"Text Shadow Width",
"UI Theme Preset":"UI Theme Preset",
"Power User Options":"Power User Options",
"Swipes":"Swipes",
"Background Sound Only":"Background Sound Only",
"Auto-load Last Chat":"Auto-load Last Chat",
"Auto-save Message Edits":"Auto-save Message Edits",
"Auto-fix Markdown":"Auto-fix Markdown",
"Allow {{char}}: in bot messages":"Allow {{char}}: in bot messages",
"Allow {{user}}: in bot messages":"Allow {{user}}: in bot messages",
"Auto-scroll Chat":"Auto-scroll Chat",
"Render Formulas":"Render Formulas",
"Send on Enter":"Send on Enter",
"Always disabled":"Always disabled",
"Automatic (desktop)":"Automatic (desktop)",
"Always enabled":"Always enabled",
"Name":"Name",
"Your Avatar":"Your Avatar",
"Extensions API:":"Extensions API:",
"SillyTavern-extras":"SillyTavern-extras",
"Auto-connect":"Auto-connect",
"Active extensions":"Active extensions",
"Extension settings":"Extension settings",
"Description":"Description",
"First message":"First message",
"Group Controls":"Group Controls",
"Group reply strategy":"Group reply strategy",
"Natural order":"Natural order",
"List order":"List order",
"Allow self responses":"Allow self responses",
"Auto Mode":"Auto Mode",
"Add Members":"Add Members",
"Current Members":"Current Members",
"text":"text",
"Delete":"Delete",
"Cancel":"Cancel",
"Advanced Defininitions":"- Advanced Defininitions",
"Personality summary":"Personality summary",
"A brief description of the personality":"A brief description of the personality",
"Scenario":"Scenario",
"Circumstances and context of the dialogue":"Circumstances and context of the dialogue",
"Talkativeness":"Talkativeness",
"How often the chracter speaks in":"How often the chracter speaks in",
"group chats!":"group chats!",
"Shy":"Shy",
"Normal":"Normal",
"Chatty":"Chatty",
"Examples of dialogue":"Examples of dialogue",
"Forms a personality more clearly":"Forms a personality more clearly",
"Save":"Save",
"World Info Editor":"World Info Editor",
"New Entry":"New Entry",
"Export":"Export",
"Delete World":"Delete World",
"Chat History":"Chat History",
"Group Chat Scenario Override":"Group Chat Scenario Override",
"All group members will use the following scenario text instead of what is specified in their character cards.":"All group members will use the following scenario text instead of what is specified in their character cards.",
"Keywords":"Keywords",
"Separate with commas":"Separate with commas",
"Secondary Required Keywords":"Secondary Required Keywords",
"Content":"Content",
"What this keyword should mean to the AI":"What this keyword should mean to the AI",
"Memo/Note":"Memo/Note",
"Not sent to AI":"Not sent to AI",
"Constant":"Constant",
"Selective":"Selective",
"Before Char":"Before Char",
"After Char":"After Char",
"Insertion Order":"Insertion Order",
" Tokens:":" Tokens:",
"Disable":"Disable",
"${characterName}":"${characterName}",
"CHAR":"CHAR",
"is typing":"is typing",
"Back to parent chat":"Back to parent chat",
"Save bookmark":"Save bookmark",
"Convert to group":"Convert to group",
"Start new chat":"Start new chat",
"View past chats":"View past chats",
"Delete messages":"Delete messages",
"Impersonate":"Impersonate",
"Regenerate":"Regenerate",
"PNG":"PNG",
"JSON":"JSON",
"WEBP":"WEBP",
"presets": "Presets",
"Message Sound": "Message Sound",
"Author's Note": "Author's Note"
},
"zh-cn": {
"clickslidertips": "点击滑块右侧数字可手动输入",
"kobldpresets": "Kobold 预设",
"guikoboldaisettings":"GUI KoboldAI 设置",
"novelaipreserts":"NovelAI预设",
"default":"默认",
"openaipresets":"OpenAI 预设",
"text gen webio(ooba) presets":"文本生成WebUI(ooba)预设",
"poe.com api settings":"poe.com API 设置",
"response legth(tokens)":"响应长度(Toekns)",
"select":"选择 ",
"context size(tokens)":"上下文大小(Toekns)",
"unlocked":"解锁",
"only select modls support context sizes greater than 2048 tokens. proceed only is you know you're doing":"仅选定模型支持大于2048Toekn的上下文窗口时可用。您在修改该选项时应该知道自己在做什么。",
"rep.pen":"Rep. Pen.",
"rep.pen range":"Rep. Pen.范围",
"temperature":"Temperature",
"Encoder Rep. Pen.":"Encoder Rep. Pen.",
"No Repeat Ngram Size":"不需要重复Ngram大小",
"Min Length":"最小长度",
"OpenAI Reverse Proxy":"OpenAI 反向代理",
"Alternative server URL (leave empty to use the default value).":"替代服务器URL(留空使用默认值)。",
"Remove your real OAI API Key from the API panel BEFORE typing anything into this box":"在输入内容之前,从API面板中删除OAI API密钥",
"We cannot provide support for problems encountered while using an unofficial OpenAI proxy":"我们无法为使用非官方OpenAI代理时遇到的问题提供支持",
"Legacy Streaming Processing":"传统流式处理",
"Enable this if the streaming doesn't work with your proxy":"如果流式响应与您的代理不兼容,请启用此功能",
"Context Size (tokens)":"上下文大小(Tokens)",
"Max Response Length (tokens)":"最大响应长度(Tokens)",
"Temperature":"采样温度",
"Frequency Penalty":"频率惩罚",
"Presence Penalty":"存在惩罚",
"Top-p":"Top-p",
"Display bot response text chunks as they are generated":"显示机器人生成的响应文本块",
"Auto-purge API context (save JB)":"自动删除应用程序接口上下文(保存JB)",
"Delete non-JB messages from Poe context before sending a new prompt. Prevents auto-jailbreak message from being pushed out of context":"在发送新提示之前从Poe上下文中删除非JB消息。防止自动越狱消息被推出上下文",
"Auto-jailbreak":"自动越狱",
"Send the jailbreak message before first generation after page refresh.":"在页面刷新后的第一次生成之前发送越狱消息",
"Send character note":"发送人物笔记",
"Sent with every prompt to modify bot responses.":"每个提示都发送给修改机器人响应的人",
"Top A":"Top-a",
"Typical Sampling":"典型采样",
"Tail Free Sampling":"无尾采样",
"Rep. Pen. Slope":"Rep. Pen. Slope",
"Single-line mode":"单行模式",
"Top K":"Top-k",
"Top P":"Top-p",
"Typical P":"典型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_token标记。禁用此功能可以让回复更加创造性.",
"Ban EOS Token":"禁止EOS标记",
"Ban the eos_token. This forces the model to never end the generation prematurely":"禁止eos_token标记。这会迫使模型不会过早结束生成",
"Skip Special Tokens":"跳过特殊标记",
"Beam search":"搜索",
"Number of Beams":"光束数目",
"Length Penalty":"长度惩罚",
"Early Stopping":"提前终止",
"Contrastive search":"对比搜索",
"Penalty Alpha":"惩罚系数",
"Seed":"种子",
"Inserts jailbreak as a last system message.":"在最后一个系统消息中插入越狱",
"This tells the AI to ignore its usual content restrictions.":"这告诉人工智能忽略其通常的内容限制",
"NSFW Encouraged":"NSFW鼓励",
"Tell the AI that NSFW is allowed.":"告诉人工智能,NSFW是允许的。",
"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.":"关闭此选项后,响应将在完成后立即显示所有响应。",
"Enhance Definitions":"增强定义",
"Use OAI knowledge base to enhance definitions for public figures and known fictional characters":"使用 OAI 知识库增强公众人物和已知虚构人物的定义",
"Wrap in Quotes":"用引号括起来",
"Wrap entire user message in quotes before sending.":"在发送之前将整个用户消息包装在引号中。",
"Leave off if you use quotes manually for speech.":"",
"Main prompt":"主提示符",
"The main prompt used to set the model behavior":"用于设置模型行为的主要提示",
"NSFW prompt":"NSFW 提示",
"Prompt that is used when the NSFW toggle is on":"NSFW 切换开关打开时使用的提示",
"Jailbreak prompt":"越狱提示",
"Prompt that is used when the Jailbreak toggle is on":"越狱切换开关打开时使用的提示",
"Impersonation prompt":"模拟提示",
"Prompt that is used for Impersonation function":"用于模拟功能的提示",
"Logit Bias":"对数偏差",
"Helps to ban or reenforce the usage of certain words":"有助于禁止或加强某些单词的使用",
"View / Edit bias preset":"查看/编辑偏置预设",
"Add bias entry":"添加偏置条目",
"Jailbreak activation message":"越狱激活消息",
"Message to send when auto-jailbreak is on.":"自动越狱开启时要发送的消息。",
"Jailbreak confirmation reply":"越狱确认回复",
"Bot must send this back to confirm jailbreak":"机器人必须将其发回以确认越狱",
"Character Note":"人物注释",
"Influences bot behavior in its responses":"影响机器人响应中的行为",
"API":"API",
"KoboldAI":"KoboldAI",
"Poe":"Poe",
"Use Horde":"使用Horde",
"API url":"接口网址",
"Register a Horde account for faster queue times":"注册帐户以加快排队时间",
"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":"接口密钥",
"Register":"注册",
"For privacy reasons":"出于隐私原因,您的 API 密钥将在您重新加载页面后隐藏",
"Model":"模型",
"Hold Control / Command key to select multiple models.":"按住控制/命令键选择多个型号。",
"Horde models not loaded":"按住控制/命令键选择多个型号。",
"Not connected":"未连接",
"Novel API key":"NovelAI API 密钥",
"Follow":"跟随",
"these directions":" 这些帮助 ",
"to get your NovelAI API key.":"以获取您的 NovelAI API 密钥。",
"Enter it in the box below":"将其输入到下面的输入框中",
"Novel AI Model":"NovelAI 模型",
"Euterpe":"Euterpe",
"Krake":"Krate",
"No connection":"无连接",
"oobabooga/text-generation-webui":"",
"Make sure you run it with":"确保启动时包含 --api 参数",
"Blocking API url":"阻塞式 API 地址",
"Streaming API url":"Streaming API 地址",
"to get your OpenAI API key.":"以获取您的 OpenAI API 密钥。",
"OpenAI Model":"OpenAI模型",
"View API Usage Metrics":"查看 API 使用情况",
"Poe.com Unofficial API":"Poe.com 非官方的 “API”",
"to get your 'p-b cookie'":"",
"Bot":"Bot",
"Connect to the API":"连接到API",
"Auto-connect to Last Server":"自动连接到最后的API",
"View hidden API keys":"查看隐藏的 API 密钥",
"Advanced Formatting":"高级格式化",
"AutoFormat Overrides":"自动套用格式替代",
"Disable description formatting":"禁用说明格式",
"Disable personality formatting":"禁用个性化格式",
"Disable scenario formatting":"禁用方案格式",
"Disable example chats formatting":"禁用聊天格式示例",
"Disable chat start formatting":"禁用聊天开始格式",
"Custom Chat Separator":"自定义聊天分隔符",
"Instruct mode":"指示模式",
"Enabled":"启用",
"Wrap Sequences with Newline":"用换行符换行序列",
"Include Names":"包括名称",
"System Prompt":"系统提示",
"Input Sequence":"输入序列",
"Output Sequence":"输出序列",
"System Sequence":"系统顺序",
"Stop Sequence":"停止序列",
"Context Formatting":"上下文格式",
"Tokenizer":"分词器",
"None / Estimated":"无/估计",
"Sentencepiece (LLaMA)":"Sentencepiece (LLaMA)",
"Token Padding":"令牌填充",
"Always add character's name to prompt":"始终将角色名称添加到提示符中",
"Keep Example Messages in Prompt":"保持示例消息提示",
"Remove Empty New Lines from Output":"从输出中删除空的新行",
"Pygmalion Formatting":"Pygmalion 格式",
"Disabled for all models":"对所有模型禁用",
"Automatic (based on model name)":"自动(基于型号名称)",
"Enabled for all models":"所有模型启用",
"Multigen":"Multigen",
"First chunk (tokens)":"第一个区块Tokens",
"Next chunks (tokens)":"接下来的区块Tokens",
"Anchors Order":"锚点顺序",
"Character then Style":"字符然后样式",
"Style then Character":"样式然后字符",
"Character Anchor":"角色锚点",
"Style Anchor":"样式锚点",
"World Info":"",
"Scan Depth":"扫描深度",
"depth":"深度",
"Token Budget":"Token预算",
"budget":"预算",
"Recursive scanning":"递归扫描",
"Soft Prompt":"软提示",
"About soft prompts":"关于软提示",
"None":"没有",
"User Settings":"用户设置",
"UI Customization":"用户界面定制",
"Avatar Style":"头像风格",
"Circle":"圈",
"Rectangle":"Rectangle",
"Chat Style":"聊天方式:",
"Default":"默认",
"Bubbles":"气泡",
"Chat Width (PC)":"聊天宽度(电脑):",
"No Blur Effect":"无模糊效果",
"No Text Shadows":"无文本阴影",
"Waifu Mode":"♡ Waifu模式 ♡",
"Message Timer":"消息计时器",
"Characters Hotswap":"角色热插拔",
"Movable UI Panels":"可移动的用户界面面板",
"Reset Panels":"重置面板",
"UI Colors":"用户界面颜色",
"Main Text":"正文",
"Italics Text":"斜体文本",
"Quote Text":"引用文本",
"Shadow Color":"阴影颜色",
"FastUI BG":"快界面 BG",
"Blur Tint":"模糊色调",
"Font Scale":"字体比例",
"Blur Strength":"模糊强度",
"Text Shadow Width":"文本阴影宽度",
"UI Theme Preset":"UI 主题预设",
"Power User Options":"高级用户选项",
"Swipes":"滑动",
"Background Sound Only":"仅背景声音",
"Auto-load Last Chat":"自动加载上次聊天",
"Auto-save Message Edits":"自动保存消息编辑",
"Auto-fix Markdown":"自动修复",
"Allow {{char}}: in bot messages":"允许 {{char}}:在机器人消息中",
"Allow {{user}}: in bot messages":"允许 {{user}}:在机器人消息中",
"Auto-scroll Chat":"自动滚动聊天",
"Render Formulas":"渲染公式",
"Send on Enter":"输入时发送",
"Always disabled":"始终禁用",
"Automatic (desktop)":"自动(桌面)",
"Always enabled":"始终启用",
"Name":"名字",
"Your Avatar":"你的头像",
"Extensions API:":"扩展接口:",
"SillyTavern-extras":"SillyTavern-extras",
"Auto-connect":"自动连接",
"Active extensions":"活动扩展",
"Extension settings":"扩展设置",
"Description":"描述",
"First message":"第一条消息",
"Group Controls":"组控件",
"Group reply strategy":"组回复策略",
"Natural order": "自然顺序",
"List order":"列表顺序",
"Allow self responses":"允许自我响应",
"Auto Mode":"自动模式",
"Add Members":"添加成员",
"Current Members":"现有成员",
"text":"文本",
"Delete":"删除",
"Cancel":"取消",
"Advanced Defininitions":"- 高级定义",
"Personality summary":"性格总结",
"A brief description of the personality":"个性的简要描述",
"Scenario":"场景",
"Circumstances and context of the dialogue":"对话的情况和背景",
"Talkativeness":"",
"How often the chracter speaks in":"说话的频率",
"group chats!":"群聊!",
"Shy":"羞涩 ",
"Normal":"正常",
"Chatty":"",
"Examples of dialogue":"对话示例",
"Forms a personality more clearly":"更清晰地形成个性",
"Save":"保存",
"World Info Editor":"信息编辑器",
"New Entry":"新一行",
"Export":"导出",
"Delete World":"删除文本",
"Chat History":"聊天记录",
"Group Chat Scenario Override":"群聊方案覆盖",
"All group members will use the following scenario text instead of what is specified in their character cards.":"所有组成员都将使用以下方案文本,而不是其角色卡中指定的内容。",
"Keywords":"关键字",
"Separate with commas":"用逗号分隔",
"Secondary Required Keywords":"次要必填关键字",
"Content":"内容",
"What this keyword should mean to the AI":"这个关键词对AI意味着什么",
"Memo/Note":"备忘录/便笺",
"Not sent to AI":"未发送到 AI",
"Constant":"常数 ",
"Selective":"选择",
"Before Char":"在Char之前",
"After Char":"在Char之后",
"Insertion Order":"顺序",
"Tokens:":"Tokens",
"Disable":"禁用",
"${characterName}":"${字符名称}",
"CHAR":"字符",
"is typing":"正在输入...",
"Back to parent chat":"返回聊天",
"Save bookmark":"保存书签",
"Convert to group":"转换为组",
"Start new chat":"开始新聊天",
"View past chats":"查看过去的聊天",
"Delete messages":"删除消息",
"Impersonate":"模拟",
"Regenerate":"重新生成",
"PNG":"PNG",
"JSON":"JSON",
"WEBP":"WEBP",
"presets": "预设",
"Message Sound": "消息音效",
"Author's Note": "作者的注释"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -119,7 +119,6 @@ import {
end_trim_to_sentence,
countOccurrences,
isOdd,
isElementInViewport,
sortMoments,
timestampToMoment,
download,
@ -260,7 +259,7 @@ export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde heade
let is_colab = false;
let is_checked_colab = false;
let is_mes_reload_avatar = false;
let optionsPopper = Popper.createPopper(document.getElementById('send_form'), document.getElementById('options'), {
let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), {
placement: 'top-start'
});
let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), {
@ -425,6 +424,9 @@ export const event_types = {
IMPERSONATE_READY: 'impersonate_ready',
CHAT_CHANGED: 'chat_id_changed',
GENERATION_STOPPED: 'generation_stopped',
SETTINGS_UPDATED: 'settings_updated',
GROUP_UPDATED: 'group_updated',
MOVABLE_PANELS_RESET: 'movable_panels_reset',
}
export const eventSource = new EventEmitter();
@ -478,22 +480,33 @@ function getTokenCount(str, padding = undefined) {
case tokenizers.CLASSIC:
return encode(str).length + padding;
case tokenizers.LLAMA:
let tokenCount = 0;
jQuery.ajax({
async: false,
type: 'POST', //
url: `/tokenize_llama`,
data: JSON.stringify({ text: str }),
dataType: "json",
contentType: "application/json",
success: function (data) {
tokenCount = data.count;
}
});
return tokenCount + padding;
return countTokensRemote('/tokenize_llama', str, padding);
case tokenizers.NERD:
return countTokensRemote('/tokenize_nerdstash', str, padding);
case tokenizers.NERD2:
return countTokensRemote('/tokenize_nerdstash_v2', str, padding);
default:
console.warn("Unknown tokenizer type", tokenizerType);
return Math.ceil(str.length / CHARACTERS_PER_TOKEN_RATIO) + padding;
}
}
function countTokensRemote(endpoint, str, padding) {
let tokenCount = 0;
jQuery.ajax({
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify({ text: str }),
dataType: "json",
contentType: "application/json",
success: function (data) {
tokenCount = data.count;
}
});
return tokenCount + padding;
}
function reloadMarkdownProcessor(render_formulas = false) {
if (render_formulas) {
converter = new showdown.Converter({
@ -550,6 +563,7 @@ let create_save_avatar = "";
let create_save_scenario = "";
let create_save_mes_example = "";
let create_save_talkativeness = talkativeness_default;
let create_save_alternate_greetings = [];
//animation right menu
let animation_duration = 250;
@ -818,7 +832,7 @@ async function printCharacters() {
template.find('.avatar').attr('title', item.avatar);
template.find('.ch_name').text(item.name);
template.find('.ch_fav_icon').css("display", 'none');
template.addClass(item.fav == "true" ? 'is_fav' : '');
template.toggleClass('is_fav', item.fav || item.fav == 'true');
template.find('.ch_fav').val(item.fav);
// Display inline tags
@ -2586,12 +2600,14 @@ function getMaxContextSize() {
} else {
this_max_context = Number(max_context);
if (nai_settings.model_novel == 'krake-v2') {
this_max_context -= 160;
// Krake has a max context of 2048
// Should be used with nerdstash tokenizer for best results
this_max_context = Math.min(max_context, 2048);
}
if (nai_settings.model_novel == 'clio-v1') {
// Clio has a max context of 8192
// TODO: Evaluate the relevance of nerdstash-v1 tokenizer, changes quite a bit.
this_max_context = 8192 - 60 - 160;
// Should be used with nerdstash_v2 tokenizer for best results
this_max_context = Math.min(max_context, 8192);
}
}
}
@ -3394,7 +3410,15 @@ async function renameCharacter() {
// Also rename as a group member
await renameGroupMember(oldAvatar, newAvatar, newValue);
callPopup('<h3>Character renamed!</h3>Sprites folder (if any) should be renamed manually.', 'text');
const renamePastChatsConfirm = await callPopup(`<h3>Character renamed!</h3>
<p>Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?</p>
<i><b>Sprites folder (if any) should be renamed manually.</b></i>`, 'confirm');
if (renamePastChatsConfirm) {
await renamePastChats(newAvatar, newValue);
await reloadCurrentChat();
toastr.success('Character renamed and past chats updated!');
}
}
else {
throw new Error('Newly renamed character was lost?');
@ -3412,6 +3436,59 @@ async function renameCharacter() {
}
}
async function renamePastChats(newAvatar, newValue) {
const pastChats = await getPastCharacterChats();
for (const { file_name } of pastChats) {
try {
const fileNameWithoutExtension = file_name.replace('.jsonl', '');
const getChatResponse = await fetch('/getchat', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
ch_name: newValue,
file_name: fileNameWithoutExtension,
avatar_url: newAvatar,
}),
cache: 'no-cache',
});
if (getChatResponse.ok) {
const currentChat = await getChatResponse.json();
for (const message of currentChat) {
if (message.is_user || message.is_system || message.extra?.type == system_message_types.NARRATOR) {
continue;
}
if (message.name !== undefined) {
message.name = newValue;
}
}
const saveChatResponse = await fetch('/savechat', {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
ch_name: newValue,
file_name: fileNameWithoutExtension,
chat: currentChat,
avatar_url: newAvatar,
}),
cache: 'no-cache',
});
if (!saveChatResponse.ok) {
throw new Error('Could not save chat');
}
}
} catch (error) {
toastr.error(`Past chat could not be updated: ${file_name}`);
console.error(error);
}
}
}
async function saveChat(chat_name, withMetadata) {
const metadata = { ...chat_metadata, ...(withMetadata || {}) };
let file_name = chat_name ?? characters[this_chid].chat;
@ -3571,13 +3648,26 @@ async function getChatResult() {
name2 = characters[this_chid].name;
if (chat.length === 0) {
const firstMes = characters[this_chid].first_mes || default_ch_mes;
const alternateGreetings = characters[this_chid]?.data?.alternate_greetings;
chat[0] = {
name: name2,
is_user: false,
is_name: true,
send_date: humanizedDateTime(),
mes: firstMes
mes: firstMes,
};
if (Array.isArray(alternateGreetings) && alternateGreetings.length > 0) {
chat[0]['swipe_id'] = 0;
chat[0]['swipes'] = [];
chat[0]['swipes'][0] = chat[0]['mes'];
for (let i = 0; i < alternateGreetings.length; i++) {
const alternateGreeting = alternateGreetings[i];
chat[0]['swipes'].push(substituteParams(alternateGreeting));
}
}
}
printMessages();
select_selected_character(this_chid);
@ -3955,6 +4045,7 @@ function selectKoboldGuiPreset() {
async function saveSettings(type) {
//console.log('Entering settings with name1 = '+name1);
eventSource.emit(event_types.SETTINGS_UPDATED);
return jQuery.ajax({
type: "POST",
url: "/savesettings",
@ -4348,12 +4439,13 @@ export function select_selected_character(chid) {
this_avatar = getThumbnailUrl('avatar', characters[chid].avatar);
}
updateFavButtonState(characters[chid].fav == "true");
updateFavButtonState(characters[chid].fav || characters[chid].fav == "true");
$("#avatar_load_preview").attr("src", this_avatar);
$("#name_div").removeClass('displayBlock');
$("#name_div").addClass('displayNone');
$("#renameCharButton").css("display", "");
$('.open_alternate_greetings').data('chid', chid);
$("#form_create").attr("actiontype", "editcharacter");
saveSettingsDebounced();
@ -4402,6 +4494,7 @@ function select_rm_create() {
$("#renameCharButton").css('display', 'none');
$("#name_div").removeClass('displayNone');
$("#name_div").addClass('displayBlock');
$('.open_alternate_greetings').data('chid', undefined);
updateFavButtonState(false);
$("#form_create").attr("actiontype", "createcharacter");
@ -4440,6 +4533,7 @@ function callPopup(text, type, inputValue = '') {
$("#dialogue_popup_ok").text("Ok");
$("#dialogue_popup_cancel").css("display", "none");
case "text":
case "alternate_greeting":
case "char_not_selected":
$("#dialogue_popup_ok").text("Ok");
$("#dialogue_popup_cancel").css("display", "none");
@ -4551,13 +4645,18 @@ function showSwipeButtons() {
if (
chat[chat.length - 1].is_system ||
!swipes ||
$('.mes:last').attr('mesid') <= 0 ||
$('.mes:last').attr('mesid') < 0 ||
chat[chat.length - 1].is_user ||
chat[chat.length - 1].extra?.image ||
count_view_mes <= 1 ||
count_view_mes < 1 ||
(selected_group && is_group_generating)
) { return; }
// swipe_id should be set if alternate greetings are added
if (chat.length == 1 && chat[0].swipe_id === undefined) {
return;
}
//had to add this to make the swipe counter work
//(copied from the onclick functions for swipe buttons..
//don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..)
@ -4736,6 +4835,241 @@ function enlargeMessageImage() {
callPopup(img.outerHTML, 'text');
}
function updateAlternateGreetingsHintVisibility(root) {
const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
$(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
}
function openAlternateGreetings() {
const chid = $('.open_alternate_greetings').data('chid');
if (menu_type != 'create' && chid === undefined) {
toastr.error('Does not have an Id for this character in editor menu.');
return;
} else {
// If the character does not have alternate greetings, create an empty array
if (chid && Array.isArray(characters[chid].data.alternate_greetings) == false) {
characters[chid].data.alternate_greetings = [];
}
}
const template = $('#alternate_greetings_template .alternate_grettings').clone();
const getArray = () => menu_type == 'create' ? create_save_alternate_greetings : characters[chid].data.alternate_greetings;
for (let index = 0; index < getArray().length; index++) {
addAlternateGreeting(template, getArray()[index], index, getArray);
}
template.find('.add_alternate_greeting').on('click', function () {
const array = getArray();
const index = array.length;
array.push(default_ch_mes);
addAlternateGreeting(template, default_ch_mes, index, getArray);
updateAlternateGreetingsHintVisibility(template);
});
updateAlternateGreetingsHintVisibility(template);
callPopup(template, 'alternate_greeting');
}
function addAlternateGreeting(template, greeting, index, getArray) {
const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
greetingBlock.find('.alternate_greeting_text').on('input', async function() {
const value = $(this).val();
const array = getArray();
array[index] = value;
}).val(greeting);
greetingBlock.find('.greeting_index').text(index + 1);
greetingBlock.find('.delete_alternate_greeting').on('click', async function () {
if (confirm('Are you sure you want to delete this alternate greeting?')) {
const array = getArray();
array.splice(index, 1);
// We need to reopen the popup to update the index numbers
openAlternateGreetings();
}
});
template.find('.alternate_greetings_list').append(greetingBlock);
}
async function createOrEditCharacter(e) {
$("#rm_info_avatar").html("");
let save_name = create_save_name;
var formData = new FormData($("#form_create").get(0));
formData.set('fav', fav_ch_checked);
if ($("#form_create").attr("actiontype") == "createcharacter") {
if ($("#character_name_pole").val().length > 0) {
//if the character name text area isn't empty (only posible when creating a new character)
let url = "/createcharacter";
if (crop_data != undefined) {
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
}
formData.delete('alternate_greetings');
for (const value of create_save_alternate_greetings) {
formData.append('alternate_greetings', value);
}
await jQuery.ajax({
type: "POST",
url: url,
data: formData,
beforeSend: function () {
$("#create_button").attr("disabled", true);
$("#create_button").attr("value", "⏳");
},
cache: false,
contentType: false,
processData: false,
success: async function (html) {
$("#character_cross").trigger('click'); //closes the advanced character editing popup
const fields = [
{ id: '#character_name_pole', callback: value => create_save_name = value },
{ id: '#description_textarea', callback: value => create_save_description = value },
{ id: '#creator_notes_textarea', callback: value => create_save_creator_notes = value },
{ id: '#character_version_textarea', callback: value => create_save_character_version = value },
{ id: '#post_history_instructions_textarea', callback: value => create_save_post_history_instructions = value },
{ id: '#system_prompt_textarea', callback: value => create_save_system_prompt = value },
{ id: '#tags_textarea', callback: value => create_save_tags = value },
{ id: '#creator_textarea', callback: value => create_save_creator = value },
{ id: '#personality_textarea', callback: value => create_save_personality = value },
{ id: '#firstmessage_textarea', callback: value => create_save_first_message = value },
{ id: '#talkativeness_slider', callback: value => create_save_talkativeness = value, defaultValue: talkativeness_default },
{ id: '#scenario_pole', callback: value => create_save_scenario = value },
{ id: '#mes_example_textarea', callback: value => create_save_mes_example = value },
{ id: '#character_json_data', callback: () => { } },
{ id: '#alternate_greetings_template', callback: value => create_save_alternate_greetings = value, defaultValue: [] },
];
fields.forEach(field => {
const fieldValue = field.defaultValue !== undefined ? field.defaultValue : '';
$(field.id).val(fieldValue);
field.callback && field.callback(fieldValue);
});
$("#character_popup_text_h3").text("Create character");
create_save_avatar = "";
$("#create_button").removeAttr("disabled");
$("#add_avatar_button").replaceWith(
$("#add_avatar_button").val("").clone(true)
);
$("#create_button").attr("value", "✅");
let oldSelectedChar = null;
if (this_chid != undefined && this_chid != "invalid-safety-id") {
oldSelectedChar = characters[this_chid].avatar;
}
console.log(`new avatar id: ${html}`);
createTagMapFromList("#tagList", html);
await getCharacters();
$("#rm_info_block").transition({ opacity: 0, duration: 0 });
var $prev_img = $("#avatar_div_div").clone();
$("#rm_info_avatar").append($prev_img);
select_rm_info(`char_create`, html, oldSelectedChar);
$("#rm_info_block").transition({ opacity: 1.0, duration: 2000 });
crop_data = undefined;
},
error: function (jqXHR, exception) {
$("#create_button").removeAttr("disabled");
},
});
} else {
$("#result_info").html("Name not entered");
}
} else {
let url = '/editcharacter';
if (crop_data != undefined) {
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
}
formData.delete('alternate_greetings');
const chid = $('.open_alternate_greetings').data('chid');
if (Array.isArray(characters[chid]?.data?.alternate_greetings)) {
for (const value of characters[chid].data.alternate_greetings) {
formData.append('alternate_greetings', value);
}
}
await jQuery.ajax({
type: "POST",
url: url,
data: formData,
beforeSend: function () {
$("#create_button").attr("disabled", true);
$("#create_button").attr("value", "Save");
},
cache: false,
contentType: false,
processData: false,
success: async function (html) {
if (chat.length === 1 && !selected_group) {
var this_ch_mes = default_ch_mes;
if ($("#firstmessage_textarea").val() != "") {
this_ch_mes = $("#firstmessage_textarea").val();
}
if (
this_ch_mes !=
$.trim(
$("#chat")
.children(".mes")
.children(".mes_block")
.children(".mes_text")
.text()
)
) {
clearChat();
chat.length = 0;
chat[0] = {};
chat[0]["name"] = name2;
chat[0]["is_user"] = false;
chat[0]["is_name"] = true;
chat[0]["mes"] = this_ch_mes;
chat[0]["extra"] = {};
const alternateGreetings = characters[this_chid]?.data?.alternate_greetings;
if (Array.isArray(alternateGreetings) && alternateGreetings.length > 0) {
chat[0]['swipe_id'] = 0;
chat[0]['swipes'] = [];
chat[0]['swipes'][0] = chat[0]['mes'];
for (let i = 0; i < alternateGreetings.length; i++) {
const alternateGreeting = alternateGreetings[i];
chat[0]['swipes'].push(substituteParams(alternateGreeting));
}
}
add_mes_without_animation = true;
//console.log('form create submission calling addOneMessage');
addOneMessage(chat[0]);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
}
}
$("#create_button").removeAttr("disabled");
await getCharacters();
$("#add_avatar_button").replaceWith(
$("#add_avatar_button").val("").clone(true)
);
$("#create_button").attr("value", "Save");
crop_data = undefined;
},
error: function (jqXHR, exception) {
$("#create_button").removeAttr("disabled");
$("#result_info").html("<font color=red>Error: no connection</font>");
console.log('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.');
toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.');
},
});
}
}
window["SillyTavern"].getContext = function () {
return {
chat: chat,
@ -4891,6 +5225,13 @@ const swipe_right = () => {
return;
}
if (chat.length == 1) {
if (chat[0]['swipe_id'] !== undefined && chat[0]['swipe_id'] == chat[0]['swipes'].length - 1) {
toastr.info('Add more alternative greetings to swipe through', 'That\'s all for now');
return;
}
}
const swipe_duration = 200;
const swipe_range = 700;
//console.log(swipe_range);
@ -5130,6 +5471,9 @@ function importCharacter(file) {
});
}
const isPwaMode = window.navigator.standalone;
if (isPwaMode) { $("body").addClass('PWA') }
$(document).ready(function () {
//////////INPUT BAR FOCUS-KEEPING LOGIC/////////////
@ -5250,7 +5594,7 @@ $(document).ready(function () {
return;
}
if (this_chid !== $(this).attr("chid")) {
if (selected_group || this_chid !== $(this).attr("chid")) {
//if clicked on a different character from what was currently selected
if (!is_send_press) {
cancelTtsPlay();
@ -5482,16 +5826,17 @@ $(document).ready(function () {
" -- Name: " +
characters[this_chid].name
);
const avatar = characters[this_chid].avatar;
const name = characters[this_chid].name;
var msg = jQuery("#form_create").serialize(); // ID form
jQuery.ajax({
method: "POST",
url: "/deletecharacter",
beforeSend: function () {
select_rm_info("char_delete", characters[this_chid].name);
},
data: msg,
cache: false,
success: function (html) {
success: async function (html) {
//RossAscends: New handling of character deletion that avoids page refreshes and should have
// fixed char corruption due to cache problems.
//due to how it is handled with 'popup_type', i couldn't find a way to make my method completely
@ -5512,7 +5857,9 @@ $(document).ready(function () {
.text(""); // removes character name from nav tabs
clearChat(); // removes deleted char's chat
this_chid = undefined; // prevents getCharacters from trying to load an invalid char.
getCharacters(); // gets the new list of characters (that doesn't include the deleted one)
delete tag_map[avatar]; // removes deleted char's avatar from tag_map
await getCharacters(); // gets the new list of characters (that doesn't include the deleted one)
select_rm_info("char_delete", name); // also updates the 'deleted character' message
printMessages(); // prints out system user's 'deleted character' message
//console.log("#dialogue_popup_ok(del-char) >>>> saving");
saveSettingsDebounced(); // saving settings to keep changes to variables
@ -5522,6 +5869,9 @@ $(document).ready(function () {
if (popup_type === "world_imported") {
selectImportedWorldInfo();
}
if (popup_type == "alternate_greeting" && menu_type !== "create") {
createOrEditCharacter();
}
if (popup_type === "del_world" && world_info) {
deleteWorldInfo(world_info);
}
@ -5571,7 +5921,6 @@ $(document).ready(function () {
dialogueResolve = null;
}
});
$("#dialogue_popup_cancel").click(function (e) {
$("#shadow_popup").transition({
@ -5603,155 +5952,7 @@ $(document).ready(function () {
read_avatar_load(this);
});
$("#form_create").submit(function (e) {
$("#rm_info_avatar").html("");
let save_name = create_save_name;
var formData = new FormData($("#form_create").get(0));
formData.set('fav', fav_ch_checked);
if ($("#form_create").attr("actiontype") == "createcharacter") {
if ($("#character_name_pole").val().length > 0) {
//if the character name text area isn't empty (only posible when creating a new character)
let url = "/createcharacter";
if (crop_data != undefined) {
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
}
jQuery.ajax({
type: "POST",
url: url,
data: formData,
beforeSend: function () {
$("#create_button").attr("disabled", true);
$("#create_button").attr("value", "⏳");
},
cache: false,
contentType: false,
processData: false,
success: async function (html) {
$("#character_cross").trigger('click'); //closes the advanced character editing popup
const fields = [
{ id: '#character_name_pole', callback: value => create_save_name = value },
{ id: '#description_textarea', callback: value => create_save_description = value },
{ id: '#creator_notes_textarea', callback: value => create_save_creator_notes = value },
{ id: '#character_version_textarea', callback: value => create_save_character_version = value },
{ id: '#post_history_instructions_textarea', callback: value => create_save_post_history_instructions = value },
{ id: '#system_prompt_textarea', callback: value => create_save_system_prompt = value },
{ id: '#tags_textarea', callback: value => create_save_tags = value },
{ id: '#creator_textarea', callback: value => create_save_creator = value },
{ id: '#personality_textarea', callback: value => create_save_personality = value },
{ id: '#firstmessage_textarea', callback: value => create_save_first_message = value },
{ id: '#talkativeness_slider', callback: value => create_save_talkativeness = value, defaultValue: talkativeness_default },
{ id: '#scenario_pole', callback: value => create_save_scenario = value },
{ id: '#mes_example_textarea', callback: value => create_save_mes_example = value },
{ id: '#character_json_data', callback: () => { } },
];
fields.forEach(field => {
const fieldValue = field.defaultValue !== undefined ? field.defaultValue : '';
$(field.id).val(fieldValue);
field.callback && field.callback(fieldValue);
});
$("#character_popup_text_h3").text("Create character");
create_save_avatar = "";
$("#create_button").removeAttr("disabled");
$("#add_avatar_button").replaceWith(
$("#add_avatar_button").val("").clone(true)
);
$("#create_button").attr("value", "✅");
let oldSelectedChar = null;
if (this_chid != undefined && this_chid != "invalid-safety-id") {
oldSelectedChar = characters[this_chid].avatar;
}
console.log(`new avatar id: ${html}`);
createTagMapFromList("#tagList", html);
await getCharacters();
$("#rm_info_block").transition({ opacity: 0, duration: 0 });
var $prev_img = $("#avatar_div_div").clone();
$("#rm_info_avatar").append($prev_img);
select_rm_info(`char_create`, html, oldSelectedChar);
$("#rm_info_block").transition({ opacity: 1.0, duration: 2000 });
crop_data = undefined;
},
error: function (jqXHR, exception) {
$("#create_button").removeAttr("disabled");
},
});
} else {
$("#result_info").html("Name not entered");
}
} else {
let url = '/editcharacter';
if (crop_data != undefined) {
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
}
jQuery.ajax({
type: "POST",
url: url,
data: formData,
beforeSend: function () {
$("#create_button").attr("disabled", true);
$("#create_button").attr("value", "Save");
},
cache: false,
contentType: false,
processData: false,
success: async function (html) {
if (chat.length === 1) {
var this_ch_mes = default_ch_mes;
if ($("#firstmessage_textarea").val() != "") {
this_ch_mes = $("#firstmessage_textarea").val();
}
if (
this_ch_mes !=
$.trim(
$("#chat")
.children(".mes")
.children(".mes_block")
.children(".mes_text")
.text()
)
) {
clearChat();
chat.length = 0;
chat[0] = {};
chat[0]["name"] = name2;
chat[0]["is_user"] = false;
chat[0]["is_name"] = true;
chat[0]["mes"] = this_ch_mes;
add_mes_without_animation = true;
//console.log('form create submission calling addOneMessage');
addOneMessage(chat[0]);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
}
}
$("#create_button").removeAttr("disabled");
await getCharacters();
$("#add_avatar_button").replaceWith(
$("#add_avatar_button").val("").clone(true)
);
$("#create_button").attr("value", "Save");
crop_data = undefined;
},
error: function (jqXHR, exception) {
$("#create_button").removeAttr("disabled");
$("#result_info").html("<font color=red>Error: no connection</font>");
console.log('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.');
toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.');
},
});
}
});
$("#form_create").submit(createOrEditCharacter);
$("#delete_button").click(function () {
popup_type = "del_ch";
@ -5777,22 +5978,22 @@ $(document).ready(function () {
});
const elementsToUpdate = {
'#description_textarea': function() { create_save_description = $("#description_textarea").val(); },
'#creator_notes_textarea': function() { create_save_creator_notes = $("#creator_notes_textarea").val(); },
'#character_version_textarea': function() { create_save_character_version = $("#character_version_textarea").val(); },
'#system_prompt_textarea': function() { create_save_system_prompt = $("#system_prompt_textarea").val(); },
'#post_history_instructions_textarea': function() { create_save_post_history_instructions = $("#post_history_instructions_textarea").val(); },
'#creator_textarea': function() { create_save_creator = $("#creator_textarea").val(); },
'#tags_textarea': function() { create_save_tags = $("#tags_textarea").val(); },
'#personality_textarea': function() { create_save_personality = $("#personality_textarea").val(); },
'#scenario_pole': function() { create_save_scenario = $("#scenario_pole").val(); },
'#mes_example_textarea': function() { create_save_mes_example = $("#mes_example_textarea").val(); },
'#firstmessage_textarea': function() { create_save_first_message = $("#firstmessage_textarea").val(); },
'#talkativeness_slider': function() { create_save_talkativeness = $("#talkativeness_slider").val(); },
'#description_textarea': function () { create_save_description = $("#description_textarea").val(); },
'#creator_notes_textarea': function () { create_save_creator_notes = $("#creator_notes_textarea").val(); },
'#character_version_textarea': function () { create_save_character_version = $("#character_version_textarea").val(); },
'#system_prompt_textarea': function () { create_save_system_prompt = $("#system_prompt_textarea").val(); },
'#post_history_instructions_textarea': function () { create_save_post_history_instructions = $("#post_history_instructions_textarea").val(); },
'#creator_textarea': function () { create_save_creator = $("#creator_textarea").val(); },
'#tags_textarea': function () { create_save_tags = $("#tags_textarea").val(); },
'#personality_textarea': function () { create_save_personality = $("#personality_textarea").val(); },
'#scenario_pole': function () { create_save_scenario = $("#scenario_pole").val(); },
'#mes_example_textarea': function () { create_save_mes_example = $("#mes_example_textarea").val(); },
'#firstmessage_textarea': function () { create_save_first_message = $("#firstmessage_textarea").val(); },
'#talkativeness_slider': function () { create_save_talkativeness = $("#talkativeness_slider").val(); },
};
Object.keys(elementsToUpdate).forEach(function(id) {
$(id).on("input", function() {
Object.keys(elementsToUpdate).forEach(function (id) {
$(id).on("input", function () {
if (menu_type == "create") {
elementsToUpdate[id]();
} else {
@ -5959,34 +6160,37 @@ $(document).ready(function () {
}
});
$("body").click(function () {
if ($("#options").css("opacity") == 1.0) {
$("#options").transition({
opacity: 0.0,
duration: 100, //animation_duration,
easing: animation_easing,
complete: function () {
$("#options").css("display", "none");
},
});
}
});
let hideOptionsTimeout;
$("#options_button").click(function () {
// this is the options button click function, shows the options menu if closed
if (
$("#options").css("display") === "none" &&
$("#options").css("opacity") == 0.0
) {
optionsPopper.update();
showBookmarksButtons();
$("#options").css("display", "block");
$("#options").transition({
opacity: 1.0, // the manual setting of CSS via JS is what allows the click-away feature to work
duration: 100,
easing: animation_easing,
complete: function () { optionsPopper.update(); },
});
function showOptions() {
showBookmarksButtons();
optionsPopper.update();
const optionsDiv = $("#options");
const optionsButtonDiv = $("#options_button");
const hideOptions = () => {
if (!optionsDiv.is(':hover') && !optionsButtonDiv.is(':hover')) {
optionsDiv.hide(200);
}
};
optionsDiv.on('mouseenter touchstart', () => clearTimeout(hideOptionsTimeout));
optionsButtonDiv.on('mouseenter touchstart', () => clearTimeout(hideOptionsTimeout));
optionsDiv.on('mouseleave', () => hideOptionsTimeout = setTimeout(hideOptions, 500));
optionsButtonDiv.on('mouseleave', () => hideOptionsTimeout = setTimeout(hideOptions, 500));
optionsDiv.show(200);
}
$("#options_button").on('mouseenter click touchstart', showOptions);
$(document).on('click touchend', (e) => {
const target = e.target;
const optionsDiv = $("#options");
const optionsButtonDiv = $("#options_button");
if (!$(target).closest(optionsDiv).length && !$(target).closest(optionsButtonDiv).length &&
target !== optionsDiv[0] && target !== optionsButtonDiv[0] &&
!optionsDiv.is(':hover') &&
!optionsButtonDiv.is(':hover')) {
optionsDiv.hide(200);
}
});
@ -6832,6 +7036,7 @@ $(document).ready(function () {
$(document).on('click', '#CloseAllWIEntries', function () {
$("#world_popup_entries_list").children().find('.up').click()
});
$(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
$(document).keyup(function (e) {
if (e.key === "Escape") {

View File

@ -450,7 +450,7 @@ dragElement(document.getElementById("WorldInfo"));
function dragElement(elmnt) {
export function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (document.getElementById(elmnt.id + "header")) { //ex: id="sheldheader"
@ -504,7 +504,7 @@ function dragElement(elmnt) {
pos3 = e.clientX; //new mouse X
pos4 = e.clientY; //new mouse Y
elmnt.setAttribute('data-dragged', 'true');
//fix over/underflows:

View File

@ -201,7 +201,7 @@ async function convertSoloToGroupChat() {
const members = [character.avatar];
const activationStrategy = group_activation_strategy.NATURAL;
const allowSelfResponses = false;
const favChecked = character.fav == 'true';
const favChecked = character.fav || character.fav == 'true';
const metadata = Object.assign({}, chat_metadata);
delete metadata.main_chat;

View File

@ -1,11 +1,11 @@
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced } from "../script.js";
import { callPopup, eventSource, event_types, extension_prompt_types, saveSettings, saveSettingsDebounced } from "../script.js";
import { isSubsetOf, debounce } from "./utils.js";
export {
getContext,
getApiUrl,
loadExtensionSettings,
runGenerationInterceptors,
defaultRequestArgs,
doExtrasFetch,
modules,
extension_settings,
ModuleWorkerWrapper,
@ -43,8 +43,10 @@ class ModuleWorkerWrapper {
const extension_settings = {
apiUrl: defaultUrl,
apiKey: '',
autoConnect: false,
disabledExtensions: [],
expressionOverrides: [],
memory: {},
note: {
default: '',
@ -65,9 +67,29 @@ let activeExtensions = new Set();
const getContext = () => window['SillyTavern'].getContext();
const getApiUrl = () => extension_settings.apiUrl;
const defaultRequestArgs = { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } };
let connectedToApi = false;
async function doExtrasFetch(endpoint, args) {
if (!args) {
args = {}
}
if (!args.method) {
Object.assign(args, { method: 'GET' });
}
if (!args.headers) {
args.headers = {}
}
Object.assign(args.headers, {
'Authorization': `Bearer ${extension_settings.apiKey}`,
'Bypass-Tunnel-Reminder': 'bypass'
});
const response = await fetch(endpoint, args);
return response;
}
async function discoverExtensions() {
try {
const response = await fetch('/discover_extensions');
@ -178,6 +200,8 @@ async function activateExtensions() {
async function connectClickHandler() {
const baseUrl = $("#extensions_url").val();
extension_settings.apiUrl = baseUrl;
const testApiKey = $("#extensions_api_key").val();
extension_settings.apiKey = testApiKey;
saveSettingsDebounced();
await connectToApi(baseUrl);
}
@ -233,7 +257,7 @@ async function connectToApi(baseUrl) {
url.pathname = '/api/modules';
try {
const getExtensionsResult = await fetch(url, defaultRequestArgs);
const getExtensionsResult = await doExtrasFetch(url);
if (getExtensionsResult.ok) {
const data = await getExtensionsResult.json();
@ -352,6 +376,7 @@ async function loadExtensionSettings(settings) {
}
$("#extensions_url").val(extension_settings.apiUrl);
$("#extensions_api_key").val(extension_settings.apiKey);
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);
// Activate offline extensions

View File

@ -1,5 +1,5 @@
import { getBase64Async } from "../../utils.js";
import { getContext, getApiUrl } from "../../extensions.js";
import { getContext, getApiUrl, doExtrasFetch } from "../../extensions.js";
export { MODULE_NAME };
const MODULE_NAME = 'caption';
@ -63,7 +63,7 @@ async function onSelectImage(e) {
const url = new URL(getApiUrl());
url.pathname = '/api/caption';
const apiResult = await fetch(url, {
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@ -93,5 +93,5 @@ jQuery(function () {
addDiceRollButton();
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
registerSlashCommand('roll', (_, value) => doDiceRoll(value), [], "<span class='monospace'>(dice formula)</span> roll the dice. For example, /roll 2d6", false, true);
registerSlashCommand('roll', (_, value) => doDiceRoll(value), ['r'], "<span class='monospace'>(dice formula)</span> roll the dice. For example, /roll 2d6", false, true);
});

View File

@ -1,9 +1,13 @@
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { deviceInfo, dragElement } from "../../RossAscends-mods.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { power_user } from "../../power-user.js";
import { onlyUnique, debounce } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
const UPDATE_INTERVAL = 2000;
const FALLBACK_EXPRESSION = 'joy';
const DEFAULT_EXPRESSIONS = [
"admiration",
"amusement",
@ -41,6 +45,241 @@ let lastMessage = null;
let spriteCache = {};
let inApiCall = false;
function isVisualNovelMode() {
return Boolean(!deviceInfo.isMobile && power_user.waifuMode && getContext().groupId);
}
async function forceUpdateVisualNovelMode() {
if (isVisualNovelMode()) {
await updateVisualNovelMode();
}
}
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
async function updateVisualNovelMode(name, expression) {
const container = $('#visual-novel-wrapper');
await visualNovelRemoveInactive(container);
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);
// calculate layer indices based on recent messages
await visualNovelUpdateLayers(container);
await Promise.allSettled(setSpritePromises);
// update again based on new sprites
if (setSpritePromises.length > 0) {
await visualNovelUpdateLayers(container);
}
}
async function visualNovelRemoveInactive(container) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const removeInactiveCharactersPromises = [];
// remove inactive characters after 1 second
container.find('.expression-holder').each((_, current) => {
const promise = new Promise(resolve => {
const element = $(current);
const avatar = element.data('avatar');
if (!group.members.includes(avatar) || group.disabled_members.includes(avatar)) {
element.fadeOut(250, () => {
element.remove();
resolve();
});
} else {
resolve();
}
});
removeInactiveCharactersPromises.push(promise);
});
await Promise.allSettled(removeInactiveCharactersPromises);
}
async function visualNovelSetCharacterSprites(container, name, expression) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const labels = await getExpressionsList();
const createCharacterPromises = [];
const setSpritePromises = [];
for (const avatar of group.members) {
const isDisabled = group.disabled_members.includes(avatar);
// skip disabled characters
if (isDisabled) {
continue;
}
const character = context.characters.find(x => x.avatar == avatar);
if (!character) {
continue;
}
let spriteFolderName = character.name;
const avatarFileName = getSpriteFolderName({ original_avatar: character.avatar });
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
// download images if not downloaded yet
if (spriteCache[spriteFolderName] === undefined) {
spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName);
}
const sprites = spriteCache[spriteFolderName];
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
const defaultSpritePath = sprites.find(x => x.label === FALLBACK_EXPRESSION)?.path;
const noSprites = sprites.length === 0;
if (expressionImage.length > 0) {
if (name == spriteFolderName) {
await validateImages(spriteFolderName, true);
setExpressionOverrideHtml(true); // <= force clear expression override input
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
const path = currentSpritePath || defaultSpritePath || '';
const img = expressionImage.find('img');
setImage(img, path);
}
expressionImage.toggleClass('hidden', noSprites);
} else {
const template = $('#expression-holder').clone();
template.attr('id', `expression-${avatar}`);
template.attr('data-avatar', avatar);
template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
$('#visual-novel-wrapper').append(template);
dragElement(template[0]);
template.toggleClass('hidden', noSprites);
setImage(template.find('img'), defaultSpritePath || '');
const fadeInPromise = new Promise(resolve => {
template.fadeIn(250, () => resolve());
});
createCharacterPromises.push(fadeInPromise);
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
setSpritePromises.push(setSpritePromise);
}
}
await Promise.allSettled(createCharacterPromises);
return setSpritePromises;
}
async function visualNovelUpdateLayers(container) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const recentMessages = context.chat.map(x => x.original_avatar).filter(x => x).reverse().filter(onlyUnique);
const filteredMembers = group.members.filter(x => !group.disabled_members.includes(x));
const layerIndices = filteredMembers.slice().sort((a, b) => recentMessages.indexOf(b) - recentMessages.indexOf(a));
const setLayerIndicesPromises = [];
const sortFunction = (a, b) => {
const avatarA = $(a).data('avatar');
const avatarB = $(b).data('avatar');
const indexA = filteredMembers.indexOf(avatarA);
const indexB = filteredMembers.indexOf(avatarB);
return indexA - indexB;
};
const containerWidth = container.width();
const pivotalPoint = containerWidth * 0.5;
let images = $('.expression-holder');
let imagesWidth = [];
images.sort(sortFunction).each(function () {
imagesWidth.push($(this).width());
});
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
let currentPosition = pivotalPoint - (totalWidth / 2);
if (totalWidth > containerWidth) {
let totalOverlap = totalWidth - containerWidth;
let totalWidthWithoutWidest = imagesWidth.reduce((a, b) => a + b, 0) - Math.max(...imagesWidth);
let overlaps = imagesWidth.map(width => (width / totalWidthWithoutWidest) * totalOverlap);
imagesWidth = imagesWidth.map((width, index) => width - overlaps[index]);
currentPosition = 0; // Reset the initial position to 0
}
images.sort(sortFunction).each((index, current) => {
const element = $(current);
// skip repositioning of dragged elements
if (element.data('dragged')) {
currentPosition += imagesWidth[index];
return;
}
const avatar = element.data('avatar');
const layerIndex = layerIndices.indexOf(avatar);
element.css('z-index', layerIndex);
element.show();
const promise = new Promise(resolve => {
element.animate({ left: currentPosition + 'px' }, 500, () => {
resolve();
});
});
currentPosition += imagesWidth[index];
setLayerIndicesPromises.push(promise);
});
await Promise.allSettled(setLayerIndicesPromises);
}
async function setLastMessageSprite(img, avatar, labels) {
const context = getContext();
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
if (lastMessage) {
const text = lastMessage.mes || '';
let spriteFolderName = lastMessage.name;
const avatarFileName = getSpriteFolderName(lastMessage);
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
const sprites = spriteCache[spriteFolderName] || [];
const label = await getExpressionLabel(text);
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
if (path) {
setImage(img, path);
}
}
}
function setImage(img, path) {
img.attr('src', path);
img.removeClass('default');
img.off('error');
img.on('error', function () {
console.debug('Error loading image', path);
$(this).off('error');
$(this).attr('src', '');
});
}
function onExpressionsShowDefaultInput() {
const value = $(this).prop('checked');
extension_settings.expressions.showDefault = value;
@ -73,11 +312,39 @@ async function moduleWorker() {
spriteCache = {};
}
const vnMode = isVisualNovelMode();
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
if (vnMode) {
$('#expression-wrapper').hide();
$('#visual-novel-wrapper').show();
} else {
$('#expression-wrapper').show();
$('#visual-novel-wrapper').hide();
}
const vnStateChanged = vnMode !== vnWrapperVisible;
if (vnStateChanged) {
lastMessage = null;
$('#visual-novel-wrapper').empty();
$("#expression-holder").css({ top: '', left: '', right: '', bottom: '', height: '', width: '', margin: '' });
}
const currentLastMessage = getLastCharacterMessage();
let spriteFolderName = currentLastMessage.name;
const avatarFileName = getSpriteFolderName(currentLastMessage);
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
// character has no expressions or it is not loaded
if (Object.keys(spriteCache).length === 0) {
await validateImages(currentLastMessage.name);
await validateImages(spriteFolderName);
lastCharacter = context.groupId || context.characterId;
}
@ -88,7 +355,8 @@ async function moduleWorker() {
lastCharacter = context.groupId || context.characterId;
if (context.groupId) {
await validateImages(currentLastMessage.name, true);
await validateImages(spriteFolderName, true);
await forceUpdateVisualNovelMode();
}
return;
@ -99,13 +367,13 @@ async function moduleWorker() {
expressionsList = null;
spriteCache = {};
expressionsList = await getExpressionsList();
await validateImages(currentLastMessage.name, true);
await validateImages(spriteFolderName, true);
await forceUpdateVisualNovelMode();
}
offlineMode.css('display', 'none');
}
// check if last message changed
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
&& lastMessage === currentLastMessage.mes) {
@ -119,32 +387,22 @@ async function moduleWorker() {
try {
inApiCall = true;
const url = new URL(getApiUrl());
url.pathname = '/api/classify';
let expression = await getExpressionLabel(currentLastMessage.mes);
const apiResult = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: currentLastMessage.mes })
});
if (apiResult.ok) {
const name = context.groupId ? currentLastMessage.name : context.name2;
const force = !!context.groupId;
const data = await apiResult.json();
let expression = data.classification[0].label;
// Character won't be angry on you for swiping
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
expression = 'joy';
}
setExpression(name, expression, force);
// If we're not already overriding the folder name, account for group chats.
if (spriteFolderName === currentLastMessage.name && !context.groupId) {
spriteFolderName = context.name2;
}
const force = !!context.groupId;
// Character won't be angry on you for swiping
if (currentLastMessage.mes == '...' && expressionsList.includes(FALLBACK_EXPRESSION)) {
expression = FALLBACK_EXPRESSION;
}
await sendExpressionCall(spriteFolderName, expression, force, vnMode);
}
catch (error) {
console.log(error);
@ -156,6 +414,61 @@ async function moduleWorker() {
}
}
function getSpriteFolderName(message) {
const context = getContext();
let avatarPath = '';
if (context.groupId) {
avatarPath = message.original_avatar || context.characters.find(x => message.force_avatar && message.force_avatar.includes(encodeURIComponent(x.avatar)))?.avatar;
}
else if (context.characterId) {
avatarPath = context.characters[context.characterId].avatar;
}
if (!avatarPath) {
return '';
}
const folderName = avatarPath.replace(/\.[^/.]+$/, "");
return folderName;
}
async function sendExpressionCall(name, expression, force, vnMode) {
if (!vnMode) {
vnMode = isVisualNovelMode();
}
if (vnMode) {
await updateVisualNovelMode(name, expression);
} else {
setExpression(name, expression, force);
}
}
async function getExpressionLabel(text) {
// Return if text is undefined, saving a costly fetch request
if (!modules.includes('classify') || !text) {
return FALLBACK_EXPRESSION;
}
const url = new URL(getApiUrl());
url.pathname = '/api/classify';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: text }),
});
if (apiResult.ok) {
const data = await apiResult.json();
return data.classification[0].label;
}
}
function getLastCharacterMessage() {
const context = getContext();
const reversedChat = context.chat.slice().reverse();
@ -165,10 +478,10 @@ function getLastCharacterMessage() {
continue;
}
return { mes: mes.mes, name: mes.name };
return { mes: mes.mes, name: mes.name, original_avatar: mes.original_avatar, force_avatar: mes.force_avatar };
}
return { mes: '', name: null };
return { mes: '', name: null, original_avatar: null, force_avatar: null };
}
function removeExpression() {
@ -265,7 +578,7 @@ async function getExpressionsList() {
url.pathname = '/api/classify/labels';
try {
const apiResult = await fetch(url, {
const apiResult = await doExtrasFetch(url, {
method: 'GET',
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
});
@ -292,11 +605,32 @@ async function setExpression(character, expression, force) {
console.debug('checking for expression images to show..');
if (sprite) {
console.debug('setting expression from character images folder');
if (force && isVisualNovelMode()) {
const context = getContext();
const group = context.groups.find(x => x.id === context.groupId);
for (const member of group.members) {
const groupMember = context.characters.find(x => x.avatar === member);
if (!groupMember) {
continue;
}
if (groupMember.name == character) {
setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
return;
}
}
}
img.attr('src', sprite.path);
img.removeClass('default');
img.off('error');
img.on('error', function () {
console.debug('Expression image error', sprite.path);
$(this).attr('src', '');
$(this).off('error');
if (force && extension_settings.expressions.showDefault) {
setDefault();
}
@ -384,6 +718,86 @@ async function onClickExpressionUpload(event) {
.trigger('click');
}
async function onClickExpressionOverrideButton() {
const context = getContext();
const currentLastMessage = getLastCharacterMessage();
const avatarFileName = getSpriteFolderName(currentLastMessage);
// If the avatar name couldn't be found, abort.
if (!avatarFileName) {
console.debug(`Could not find filename for character with name ${currentLastMessage.name} and ID ${context.characterId}`);
return;
}
const overridePath = $("#expression_override").val();
const existingOverrideIndex = extension_settings.expressionOverrides.findIndex((e) =>
e.name == avatarFileName
);
// If the path is empty, delete the entry from overrides
if (overridePath === undefined || overridePath.length === 0) {
if (existingOverrideIndex === -1) {
return;
}
extension_settings.expressionOverrides.splice(existingOverrideIndex, 1);
console.debug(`Removed existing override for ${avatarFileName}`);
} else {
// Properly override objects and clear the sprite cache of the previously set names
const existingOverride = extension_settings.expressionOverrides[existingOverrideIndex];
if (existingOverride) {
Object.assign(existingOverride, { path: overridePath });
delete spriteCache[existingOverride.name];
} else {
const characterOverride = { name: avatarFileName, path: overridePath };
extension_settings.expressionOverrides.push(characterOverride);
delete spriteCache[currentLastMessage.name];
}
console.debug(`Added/edited expression override for character with filename ${avatarFileName} to folder ${overridePath}`);
}
saveSettingsDebounced();
// Refresh sprites list. Assume the override path has been properly handled.
try {
$('#visual-novel-wrapper').empty();
await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
const expression = await getExpressionLabel(currentLastMessage.mes);
await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true);
forceUpdateVisualNovelMode();
} catch (error) {
console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
}
}
async function onClickExpressionOverrideRemoveAllButton() {
// Remove all the overrided entries from sprite cache
for (const element of extension_settings.expressionOverrides) {
delete spriteCache[element.name];
}
extension_settings.expressionOverrides = [];
saveSettingsDebounced();
console.debug("All expression image overrides have been cleared.");
// Refresh sprites list to use the default name if applicable
try {
$('#visual-novel-wrapper').empty();
const currentLastMessage = getLastCharacterMessage();
await validateImages(currentLastMessage.name, true);
const expression = await getExpressionLabel(currentLastMessage.mes);
await sendExpressionCall(currentLastMessage.name, expression, true);
forceUpdateVisualNovelMode();
console.debug(extension_settings.expressionOverrides);
} catch (error) {
console.debug(`The current expression could not be set because of error: ${error}`);
}
}
async function onClickExpressionUploadPackButton() {
const name = $('#image_list').data('name');
@ -439,6 +853,28 @@ async function onClickExpressionDelete(event) {
await validateImages(name);
}
function setExpressionOverrideHtml(forceClear = false) {
const currentLastMessage = getLastCharacterMessage();
const avatarFileName = getSpriteFolderName(currentLastMessage);
if (!avatarFileName) {
return;
}
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
$("#expression_override").val(expressionOverride.path);
} else if (expressionOverride) {
delete extension_settings.expressionOverrides[expressionOverride.name];
}
if (forceClear && !expressionOverride) {
$("#expression_override").val("");
}
}
(function () {
function addExpressionImage() {
const html = `
@ -450,6 +886,14 @@ async function onClickExpressionDelete(event) {
</div>`;
$('body').append(html);
}
function addVisualNovelMode() {
const html = `
<div id="visual-novel-wrapper">
</div>`
const element = $(html);
element.hide();
$('body').append(element);
}
function addSettings() {
const html = `
@ -461,12 +905,20 @@ async function onClickExpressionDelete(event) {
</div>
<div class="inline-drawer-content">
<p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p>
<div class="flex-container flexnowrap">
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
</div>
<div id="image_list"></div>
<div class="expression_buttons">
<div class="expression_buttons flex-container spaceEvenly">
<div id="expression_upload_pack_button" class="menu_button">
<i class="fa-solid fa-file-zipper"></i>
<span>Upload sprite pack (ZIP)</span>
</div>
<div id="expression_override_cleanup_button" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
<span>Remove all image overrides</span>
</div>
</div>
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
@ -480,18 +932,32 @@ async function onClickExpressionDelete(event) {
</div>
`;
$('#extensions_settings').append(html);
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
$('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
$(document).on('click', '.expression_list_item', onClickExpressionImage);
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
$(window).on("resize", updateVisualNovelModeDebounced);
$('.expression_settings').hide();
}
addExpressionImage();
addVisualNovelMode();
addSettings();
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
const updateFunction = wrapper.update.bind(wrapper);
setInterval(updateFunction, UPDATE_INTERVAL);
moduleWorker();
eventSource.on(event_types.CHAT_CHANGED, () => {
setExpressionOverrideHtml();
if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty();
}
});
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
})();

View File

@ -10,6 +10,27 @@
width: 100vw;
}
#visual-novel-wrapper {
display: flex;
height: calc(100vh - 40px);
width: 100vw;
position: relative;
overflow: hidden;
}
#visual-novel-wrapper .expression-holder {
width: max-content;
}
#visual-novel-wrapper .hidden {
display: none !important;
visibility: hidden !important;
}
/*#visual-novel-wrapper img.expression {
object-fit: cover;
}*/
.expression-holder {
min-width: 100px;
min-height: 100px;
@ -54,6 +75,7 @@ img.expression.default {
.expression_list_item {
position: relative;
max-width: 20%;
min-width: 100px;
max-height: 200px;
background-color: #515151b0;
border-radius: 10px;
@ -87,10 +109,16 @@ img.expression.default {
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 20%;
/* height: 20%; */
padding: 0.25rem;
}
.menu_button.expression_list_delete,
.menu_button.expression_list_upload {
margin-top: 0;
margin-bottom: 0;
}
.expression_list_image {
max-width: 100%;
height: 100%;

View File

@ -1,6 +1,6 @@
import { saveSettingsDebounced, getCurrentChatId, system_message_types, eventSource, event_types } from "../../../script.js";
import { humanizedDateTime } from "../../RossAscends-mods.js";
import { getApiUrl, extension_settings, getContext } from "../../extensions.js";
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
import { getFileText, onlyUnique, splitRecursive, IndexedDBStore } from "../../utils.js";
export { MODULE_NAME };
@ -174,7 +174,7 @@ async function addMessages(chat_id, messages) {
meta: JSON.stringify(m),
}));
const addMessagesResult = await fetch(url, {
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id, messages: transformedMessages }),
@ -222,7 +222,7 @@ async function onPurgeClick() {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/purge';
const purgeResult = await fetch(url, {
const purgeResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id }),
@ -242,7 +242,7 @@ async function onExportClick() {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/export';
const exportResult = await fetch(url, {
const exportResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id: currentChatId }),
@ -285,7 +285,7 @@ async function onSelectImportFile(e) {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/import';
const importResult = await fetch(url, {
const importResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify(imported),
@ -313,7 +313,7 @@ async function queryMessages(chat_id, query) {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/query';
const queryMessagesResult = await fetch(url, {
const queryMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id, query, n_results: extension_settings.chromadb.n_results }),
@ -366,7 +366,7 @@ async function onSelectInjectFile(e) {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb';
const addMessagesResult = await fetch(url, {
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id: currentChatId, messages: messages }),
@ -461,19 +461,20 @@ jQuery(async () => {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<p>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</p>
<span>Memory Injection Strategy</span>
<small>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</small>
<span class="wide100p marginTopBot5 displayBlock">Memory Injection Strategy</span>
<hr>
<select id="chromadb_strategy">
<option value="original">Replace non-kept chat items with memories</option>
<option value="ross">Add memories after chat with a header tag</option>
</select>
<label for="chromadb_keep_context">How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</label>
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
<label for="chromadb_n_results">Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</label>
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
<label for="chromadb_split_length">Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</label>
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>
<input id="chromadb_split_length" type="range" min="${defaultSettings.split_length_min}" max="${defaultSettings.split_length_max}" step="${defaultSettings.split_length_step}" value="${defaultSettings.split_length}" />
<label for="chromadb_file_split_length">Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</label>
<label for="chromadb_file_split_length"><small>Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</small></label>
<input id="chromadb_file_split_length" type="range" min="${defaultSettings.file_split_length_min}" max="${defaultSettings.file_split_length_max}" step="${defaultSettings.file_split_length_step}" value="${defaultSettings.file_split_length}" />
<label class="checkbox_label" for="chromadb_freeze" title="Pauses the automatic synchronization of new messages with ChromaDB. Older messages and injections will still be pulled as usual." >
<input type="checkbox" id="chromadb_freeze" />
@ -503,7 +504,7 @@ jQuery(async () => {
<form><input id="chromadb_import_file" type="file" accept="application/json" hidden></form>
</div>`;
$('#extensions_settings').append(settingsHtml);
$('#extensions_settings2').append(settingsHtml);
$('#chromadb_strategy').on('change', onStrategyChange);
$('#chromadb_keep_context').on('input', onKeepContextInput);
$('#chromadb_n_results').on('input', onNResultsInput);

View File

@ -1,5 +1,5 @@
import { getStringHash, debounce } from "../../utils.js";
import { getContext, getApiUrl, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { getContext, getApiUrl, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { extension_prompt_types, is_send_press, saveSettingsDebounced } from "../../../script.js";
export { MODULE_NAME };
@ -232,7 +232,7 @@ async function summarizeChat(context) {
const url = new URL(getApiUrl());
url.pathname = '/api/summarize';
const apiResult = await fetch(url, {
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -321,24 +321,24 @@ $(document).ready(function () {
<div id="memory_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Chat memory</b>
<b>Summarize</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="memory_contents">Memory contents</label>
<label for="memory_contents">Current summary: </label>
<textarea id="memory_contents" class="text_pole" rows="8" placeholder="Context will be generated here..."></textarea>
<div class="memory_contents_controls">
<input id="memory_restore" class="menu_button" type="submit" value="Restore previous state" />
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" /> Freeze context</label>
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Stop summarization updates</label>
</div>
</div>
<!--</div>
</div>
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Summarization parameters</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="inline-drawer-content">-->
<label for="memory_short_length">Buffer <small>[short-term]</small> length (<span id="memory_short_length_tokens"></span> tokens)</label>
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
<label for="memory_long_length">Summary <small>[long-term]</small> length (<span id="memory_long_length_tokens"></span> tokens)</label>
@ -353,7 +353,7 @@ $(document).ready(function () {
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#extensions_settings2').append(settingsHtml);
$('#memory_restore').on('click', onMemoryRestoreClick);
$('#memory_contents').on('input', onMemoryContentInput);
$('#memory_long_length').on('input', onMemoryLongInput);

View File

@ -66,7 +66,6 @@ function addTask(description, index = null) {
saveState()
}
// Return the task and index or throw an error
function getTaskById(taskId){
if (taskId == null) {
@ -77,11 +76,13 @@ function getTaskById(taskId){
return { task: globalTasks[index], index: index };
} else {
throw `Cannot find task with ${taskId}`
}
}
function deleteTask(taskId){
const { task, index } = getTaskById(taskId)
globalTasks.splice(index, 1)
setCurrentTask()
updateUiTaskList()
@ -325,6 +326,7 @@ function debugObjectiveExtension() {
window.debugObjectiveExtension = debugObjectiveExtension
// Populate UI task list
function updateUiTaskList() {
$('#objective-tasks').empty()
@ -334,7 +336,7 @@ function updateUiTaskList() {
task.addUiElement()
}
} else {
// Show button to add tasks if there are none
// Show button to add tasks if there are none
$('#objective-tasks').append(`
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
`)
@ -362,7 +364,7 @@ function onChatDepthInput() {
// Update how often we check for task completion
function onCheckFrequencyInput() {
checkCounter = $("#objective-check-frequency").val()
checkCounter = $("#objective-check-frequency").val()
$('#objective-counter').text(checkCounter)
saveState()
}
@ -434,20 +436,20 @@ jQuery(() => {
<div class="inline-drawer-content">
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
<div class="objective_block">
<input id="objective-generate" class="menu_button" type="submit" value="Generate Tasks" />
<small>Automatically generate tasks for Objective. Takes a moment.</small>
<div class="objective_block flex-container">
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
</div>
</br>
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label><br>
<div id="objective-tasks"> </div>
<div class="objective_block">
<div class="objective_block objective_block_control flex1">
<label for="objective-chat-depth">In-chat @ Depth</label>
<div class="objective_block margin-bot-10px">
<div class="objective_block objective_block_control flex1 flexFlowColumn">
<label for="objective-chat-depth">Position in Chat</label>
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</div>
<br>
<div class="objective_block objective_block_control flex1">
<label for="objective-check-frequency">Task Check Frequency</label>
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
<small>(0 = disabled)</small>

View File

@ -5,9 +5,10 @@
.objective_block {
display: flex;
flex-direction: row;
/* flex-direction: row; */
align-items: center;
column-gap: 10px;
column-gap: 5px;
flex-wrap: wrap;
}
.objective_block_control small,
@ -34,3 +35,8 @@
[id^=objective-task-delete-] {
color: #da3f3f;
}
#objective-tasks span {
margin: unset;
margin-bottom: 5px !important;
}

View File

@ -121,30 +121,30 @@ jQuery(async () => {
Enable Quick Replies
</label>
<small><i>Customize your Quick Replies:</i></small><br>
<div class="flex-container">
<input class="text_pole widthUnset" id="quickReply1Label" placeholder="(Add a button label)">
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply1Label" placeholder="(Add a button label)">
<textarea id="quickReply1Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
</div>
<div class="flex-container">
<input class="text_pole widthUnset" id="quickReply2Label" placeholder="(Add a button label)">
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply2Label" placeholder="(Add a button label)">
<textarea id="quickReply2Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
</div>
<div class="flex-container">
<input class="text_pole widthUnset" id="quickReply3Label" placeholder="(Add a button label)">
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply3Label" placeholder="(Add a button label)">
<textarea id="quickReply3Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
</div>
<div class="flex-container">
<input class="text_pole widthUnset" id="quickReply4Label" placeholder="(Add a button label)">
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply4Label" placeholder="(Add a button label)">
<textarea id="quickReply4Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
</div>
<div class="flex-container">
<input class="text_pole widthUnset" id="quickReply5Label" placeholder="(Add a button label)">
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply5Label" placeholder="(Add a button label)">
<textarea id="quickReply5Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
</div>
</div>
</div>`;
$('#extensions_settings').append(settingsHtml);
$('#extensions_settings2').append(settingsHtml);
$('#quickReply1Mes').on('input', function () { onQuickReplyInput(1); });
$('#quickReply2Mes').on('input', function () { onQuickReplyInput(2); });

View File

@ -41,7 +41,4 @@
opacity: 1;
filter: brightness(1.2);
cursor: pointer;
}

View File

@ -0,0 +1,110 @@
// Borrowed from Agnai (AGPLv3)
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
function capitalizeInterim(interimTranscript) {
let capitalizeIndex = -1;
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
else if (interimTranscript.length > 1) capitalizeIndex = 0;
if (capitalizeIndex > -1) {
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
const rest = interimTranscript.substring(capitalizeIndex + 1);
interimTranscript = spacing + capitalized + rest;
}
return interimTranscript;
}
function composeValues(previous, interim) {
let spacing = '';
if (previous.endsWith('.')) spacing = ' ';
return previous + spacing + interim;
}
(function ($) {
$.fn.speechRecognitionPlugin = function (options) {
const settings = $.extend({
grammar: '' // Custom grammar
}, options);
const speechRecognition = window.SpeechRecognition || webkitSpeechRecognition;
const speechRecognitionList = window.SpeechGrammarList || webkitSpeechGrammarList;
if (!speechRecognition) {
console.warn('Speech recognition is not supported in this browser.');
return;
}
const recognition = new speechRecognition();
if (settings.grammar) {
speechRecognitionList.addFromString(settings.grammar, 1);
recognition.grammars = speechRecognitionList;
}
recognition.continuous = true;
recognition.interimResults = true;
// TODO: This should be configurable.
recognition.lang = 'en-US'; // Set the language to English (US).
const $textarea = this;
const $button = $('<div class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
$('#send_but_sheld').prepend($button);
let listening = false;
$button.on('click', function () {
if (listening) {
recognition.stop();
} else {
recognition.start();
}
listening = !listening;
});
let initialText = '';
recognition.onresult = function (speechEvent) {
let finalTranscript = '';
let interimTranscript = ''
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
const transcript = speechEvent.results[i][0].transcript;
if (speechEvent.results[i].isFinal) {
let interim = capitalizeInterim(transcript);
if (interim != '') {
let final = finalTranscript;
final = composeValues(final, interim) + '.';
finalTranscript = final;
recognition.abort();
listening = false;
}
interimTranscript = ' ';
} else {
interimTranscript += transcript;
}
}
interimTranscript = capitalizeInterim(interimTranscript);
$textarea.val(initialText + finalTranscript + interimTranscript);
};
recognition.onerror = function (event) {
console.error('Error occurred in recognition:', event.error);
};
recognition.onend = function () {
listening = false;
$button.toggleClass('fa-microphone fa-microphone-slash');
};
recognition.onstart = function () {
initialText = $textarea.val();
$button.toggleClass('fa-microphone fa-microphone-slash');
};
};
}(jQuery));
jQuery(() => {
const $textarea = $('#send_textarea');
$textarea.speechRecognitionPlugin();
});

View File

@ -0,0 +1,11 @@
{
"display_name": "Speech Recognition",
"loading_order": 13,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -0,0 +1,3 @@
.speech-toggle {
display: flex;
}

View File

@ -10,7 +10,7 @@ import {
eventSource,
appendImageToMessage
} from "../../../script.js";
import { getApiUrl, getContext, extension_settings, defaultRequestArgs, modules } from "../../extensions.js";
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
import { stringFormat, initScrollHeight, resetScrollHeight } from "../../utils.js";
export { MODULE_NAME };
@ -234,7 +234,7 @@ async function onModelChange() {
async function updateExtrasRemoteModel() {
const url = new URL(getApiUrl());
url.pathname = '/api/image/model';
const getCurrentModelResult = await fetch(url, {
const getCurrentModelResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ model: extension_settings.sd.model }),
@ -285,7 +285,7 @@ async function loadExtrasSamplers() {
const url = new URL(getApiUrl());
url.pathname = '/api/image/samplers';
const result = await fetch(url, defaultRequestArgs);
const result = await doExtrasFetch(url);
if (result.ok) {
const data = await result.json();
@ -338,7 +338,7 @@ async function loadExtrasModels() {
const url = new URL(getApiUrl());
url.pathname = '/api/image/model';
const getCurrentModelResult = await fetch(url, defaultRequestArgs);
const getCurrentModelResult = await doExtrasFetch(url);
if (getCurrentModelResult.ok) {
const data = await getCurrentModelResult.json();
@ -346,7 +346,7 @@ async function loadExtrasModels() {
}
url.pathname = '/api/image/models';
const getModelsResult = await fetch(url, defaultRequestArgs);
const getModelsResult = await doExtrasFetch(url);
if (getModelsResult.ok) {
const data = await getModelsResult.json();
@ -493,7 +493,7 @@ async function generateExtrasImage(prompt, callback) {
console.log(extension_settings.sd);
const url = new URL(getApiUrl());
url.pathname = '/api/image';
const result = await fetch(url, {
const result = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({
@ -633,11 +633,11 @@ function isConnectedToExtras() {
async function moduleWorker() {
if (isConnectedToExtras() || extension_settings.sd.horde) {
$('#sd_gen').show(200);
$('#sd_gen').show();
$('.sd_message_gen').show();
}
else {
$('#sd_gen').hide(200);
$('#sd_gen').hide();
$('.sd_message_gen').hide();
}
}

View File

@ -350,7 +350,7 @@ jQuery(() => {
Translate Chat
</div>`;
$('#extensionsMenu').append(buttonHtml);
$('#extensions_settings').append(html);
$('#extensions_settings2').append(html);
$('#translate_chat').on('click', onTranslateChatClick);
$('#translation_clear').on('click', onTranslationsClearClick);

View File

@ -0,0 +1,142 @@
import { getRequestHeaders } from "../../../script.js"
import { getApiUrl } from "../../extensions.js"
import { doExtrasFetch, modules } from "../../extensions.js"
import { getPreviewString } from "./index.js"
export { EdgeTtsProvider }
class EdgeTtsProvider {
//########//
// Config //
//########//
settings
voices = []
separator = ' . '
audioElement = document.createElement('audio')
defaultSettings = {
voiceMap: {}
}
get settingsHtml() {
let html = `Microsoft Edge TTS Provider<br>`
return html
}
onSettingsChange() {
}
loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
}
console.info("Settings loaded")
}
async onApplyClick() {
return
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceIds()
}
const match = this.voices.filter(
voice => voice.name == voiceName
)[0]
if (!match) {
throw `TTS Voice name ${voiceName} not found`
}
return match
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId)
return response
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
throwIfModuleMissing()
const url = new URL(getApiUrl());
url.pathname = `/api/edge-tts/list`
const response = await doExtrasFetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}
let responseJson = await response.json()
responseJson = responseJson
.sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
.map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
return responseJson
}
async previewTtsVoice(id) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
const voice = await this.getVoice(id);
const text = getPreviewString(voice.lang);
const response = await this.fetchTtsGeneration(text, id)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}
const audio = await response.blob();
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
}
async fetchTtsGeneration(inputText, voiceId) {
throwIfModuleMissing()
console.info(`Generating new TTS for voice_id ${voiceId}`)
const url = new URL(getApiUrl());
url.pathname = `/api/edge-tts/generate`;
const response = await doExtrasFetch(url,
{
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
"text": inputText,
"voice": voiceId,
})
}
)
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response
}
}
function throwIfModuleMissing() {
if (!modules.includes('edge-tts')) {
toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`)
throw new Error(`Edge TTS module not loaded.`)
}
}

View File

@ -149,7 +149,7 @@ class ElevenLabsTtsProvider {
headers: headers
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}
const responseJson = await response.json()
return responseJson.voices
@ -166,7 +166,7 @@ class ElevenLabsTtsProvider {
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}
return response.json()
}
@ -193,7 +193,8 @@ class ElevenLabsTtsProvider {
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response
}
@ -209,7 +210,7 @@ class ElevenLabsTtsProvider {
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}
return response
}
@ -222,7 +223,7 @@ class ElevenLabsTtsProvider {
headers: headers
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
}
const responseJson = await response.json()
return responseJson.history

View File

@ -1,9 +1,11 @@
import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js'
import { ModuleWorkerWrapper, extension_settings, getContext } from '../../extensions.js'
import { getStringHash } from '../../utils.js'
import { EdgeTtsProvider } from './edge.js'
import { ElevenLabsTtsProvider } from './elevenlabs.js'
import { SileroTtsProvider } from './silerotts.js'
import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js'
const UPDATE_INTERVAL = 1000
@ -15,11 +17,53 @@ let lastGroupId = null
let lastChatId = null
let lastMessageHash = null
export function getPreviewString(lang) {
const previewStrings = {
'en-US': 'The quick brown fox jumps over the lazy dog',
'en-GB': 'Sphinx of black quartz, judge my vow',
'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
'it-IT': "Pranzo d'acqua fa volti sghembi",
'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
'pt-BR': 'Vejo xá gritando que fez show sem playback.',
'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
'uk-UA': "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!",
'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig',
'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy',
'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny',
'hu-HU': 'Árvíztűrő tükörfúrógép',
'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi',
'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten',
'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn',
'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon',
'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす',
'ko-KR': '가나다라마바사아자차카타파하',
'zh-CN': '我能吞下玻璃而不伤身体',
'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix',
'bg-BG': 'Щъркелите се разпръснаха по цялото небе',
'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός',
'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?',
'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"',
'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng',
'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa',
'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ',
'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh',
'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة',
'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा',
}
const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
return previewStrings[lang] ?? fallbackPreview;
}
let ttsProviders = {
ElevenLabs: ElevenLabsTtsProvider,
Silero: SileroTtsProvider,
System: SystemTtsProvider,
Edge: EdgeTtsProvider,
Novel: NovelTtsProvider,
}
let ttsProvider
let ttsProviderName
@ -202,7 +246,7 @@ async function playAudioData(audioBlob) {
window['tts_preview'] = function (id) {
const audio = document.getElementById(id)
if (!$(audio).data('disabled')) {
if (audio && !$(audio).data('disabled')) {
audio.play()
}
else {
@ -223,7 +267,9 @@ async function onTtsVoicesClick() {
<b class="voice_name">${voice.name}</b>
<i onclick="tts_preview('${voice.voice_id}')" class="fa-solid fa-play"></i>
</div>`
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
if (voice.preview_url) {
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
}
}
} catch {
popupText = 'Could not load voices list. Check your API key.'
@ -285,7 +331,7 @@ function completeCurrentAudioJob() {
*/
async function addAudioJob(response) {
const audioData = await response.blob()
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave']) {
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
}
audioJobQueue.push(audioData)
@ -372,6 +418,7 @@ async function processTtsQueue() {
const voice = await ttsProvider.getVoice((voiceMap[char]))
const voiceId = voice.voice_id
if (voiceId == null) {
toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`)
throw `Unable to attain voiceId for ${char}`
}
tts(text, voiceId)
@ -452,7 +499,6 @@ async function voicemapIsValid(parsedVoiceMap) {
async function updateVoiceMap() {
let isValidResult = false
const context = getContext()
const value = $('#tts_voice_map').val()
const parsedVoiceMap = parseVoiceMap(value)

View File

@ -0,0 +1,130 @@
import { getRequestHeaders } from "../../../script.js"
import { getPreviewString } from "./index.js"
export { NovelTtsProvider }
class NovelTtsProvider {
//########//
// Config //
//########//
settings
voices = []
separator = ' . '
audioElement = document.createElement('audio')
defaultSettings = {
voiceMap: {}
}
get settingsHtml() {
let html = `Use NovelAI's TTS engine.<br>
The Voice IDs in the preview list are only examples, as it can be any string of text. Feel free to try different options!<br>
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>`;
return html;
}
onSettingsChange() {
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
}
console.info("Settings loaded")
}
async onApplyClick() {
return
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (!voiceName) {
throw `TTS Voice name not provided`
}
return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false}
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId)
return response
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
const voices = [
{ name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false },
{ name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false },
{ name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false },
{ name: 'Claea', voice_id: 'Claea', lang: 'en-US', preview_url: false },
{ name: 'Lim', voice_id: 'Lim', lang: 'en-US', preview_url: false },
{ name: 'Aurae', voice_id: 'Aurae', lang: 'en-US', preview_url: false },
{ name: 'Naia', voice_id: 'Naia', lang: 'en-US', preview_url: false },
{ name: 'Aulon', voice_id: 'Aulon', lang: 'en-US', preview_url: false },
{ name: 'Elei', voice_id: 'Elei', lang: 'en-US', preview_url: false },
{ name: 'Ogma', voice_id: 'Ogma', lang: 'en-US', preview_url: false },
{ name: 'Raid', voice_id: 'Raid', lang: 'en-US', preview_url: false },
{ name: 'Pega', voice_id: 'Pega', lang: 'en-US', preview_url: false },
{ name: 'Lam', voice_id: 'Lam', lang: 'en-US', preview_url: false },
];
return voices;
}
async previewTtsVoice(id) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
const text = getPreviewString('en-US')
const response = await this.fetchTtsGeneration(text, id)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const audio = await response.blob();
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
}
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch(`/novel_tts`,
{
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
"text": inputText,
"voice": voiceId,
})
}
)
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response
}
}

View File

@ -1,4 +1,4 @@
import { getApiUrl, modules } from "../../extensions.js"
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
export { SileroTtsProvider }
@ -94,7 +94,7 @@ class SileroTtsProvider {
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
const response = await fetch(`${this.settings.provider_endpoint}/speakers`)
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
@ -104,7 +104,7 @@ class SileroTtsProvider {
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch(
const response = await doExtrasFetch(
`${this.settings.provider_endpoint}/generate`,
{
method: 'POST',
@ -118,25 +118,15 @@ class SileroTtsProvider {
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response
}
// Interface not used by Silero TTS
async fetchTtsFromHistory(history_item_id) {
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`)
const response = await fetch(
`https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,
{
headers: {
'xi-api-key': this.API_KEY
}
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
}
return response
return Promise.resolve(history_item_id);
}
}

View File

@ -1,3 +1,5 @@
import { getPreviewString } from "./index.js";
export { SystemTtsProvider }
/**
@ -74,20 +76,6 @@ class SystemTtsProvider {
// Config //
//########//
previewStrings = {
'en-US': 'The quick brown fox jumps over the lazy dog',
'en-GB': 'Sphinx of black quartz, judge my vow',
'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
'it-IT': "Pranzo d'acqua fa volti sghembi",
'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
'pt-BR': 'Vejo xá gritando que fez show sem playback.',
'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
'uk-UA': "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!",
}
fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
settings
voices = []
separator = ' ... '
@ -172,7 +160,7 @@ class SystemTtsProvider {
}
speechSynthesis.cancel();
const text = this.previewStrings[voice.lang] ?? this.fallbackPreview;
const text = getPreviewString(voice.lang);
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = voice;
utterance.rate = 1;

View File

@ -57,7 +57,7 @@ import {
event_types,
getCurrentChatId,
} from "../script.js";
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect } from './tags.js';
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
export {
selected_group,
@ -179,14 +179,24 @@ export async function getGroupChat(groupId) {
}
function getFirstCharacterMessage(character) {
let messageText = character.first_mes;
// if there are alternate greetings, pick one at random
if (Array.isArray(character.data?.alternate_greetings)) {
const messageTexts = [character.first_mes, ...character.data.alternate_greetings].filter(x => x);
messageText = messageTexts[Math.floor(Math.random() * messageTexts.length)];
}
const mes = {};
mes["is_user"] = false;
mes["is_system"] = false;
mes["name"] = character.name;
mes["is_name"] = true;
mes["send_date"] = humanizedDateTime();
mes["mes"] = character.first_mes
? substituteParams(character.first_mes.trim(), name1, character.name)
mes["original_avatar"] = character.avatar;
mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 };
mes["mes"] = messageText
? substituteParams(messageText.trim(), name1, character.name)
: default_ch_mes;
mes["force_avatar"] =
character.avatar != "none"
@ -784,6 +794,7 @@ async function deleteGroup(id) {
if (response.ok) {
selected_group = null;
delete tag_map[id];
resetChatState();
clearChat();
printMessages();
@ -954,7 +965,7 @@ function select_group_chats(groupId, skipAnimation) {
template.find(".avatar img").attr("title", character.avatar);
template.find(".ch_name").text(character.name);
template.attr("chid", characters.indexOf(character));
template.addClass(character.fav == 'true' ? 'is_fav' : '');
template.toggleClass('is_fav', character.fav || character.fav == 'true');
if (!group) {
template.find('[data-action="speak"]').hide();
@ -1002,7 +1013,7 @@ function select_group_chats(groupId, skipAnimation) {
}
$("#dialogue_popup").data("group_id", groupId);
callPopup("<h3>Delete the group?</h3>", "del_group");
callPopup('<h3>Delete the group?</h3><p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>', "del_group");
});
updateFavButtonState(group?.fav ?? false);
@ -1084,6 +1095,7 @@ function select_group_chats(groupId, skipAnimation) {
}
sortGroupMembers("#rm_group_add_members .group_member");
await eventSource.emit(event_types.GROUP_UPDATED);
});
}

View File

@ -9,6 +9,8 @@ import {
getRequestHeaders,
substituteParams,
updateVisibleDivs,
eventSource,
event_types,
} from "../script.js";
import { favsToHotswap } from "./RossAscends-mods.js";
import {
@ -58,6 +60,8 @@ const tokenizers = {
GPT3: 1,
CLASSIC: 2,
LLAMA: 3,
NERD: 4,
NERD2: 5,
}
const send_on_enter_options = {
@ -836,6 +840,9 @@ function resetMovablePanels() {
document.getElementById("WorldInfo").style.height = '';
document.getElementById("WorldInfo").style.width = '';
document.getElementById("WorldInfo").style.margin = '';
$('*[data-dragged="true"]').removeAttr('data-dragged');
eventSource.emit(event_types.MOVABLE_PANELS_RESET);
}
$(document).ready(() => {

View File

@ -340,7 +340,7 @@ code {
margin: 0 auto;
left: 0;
right: 0;
z-index: 3;
z-index: 30;
min-height: 100px;
min-width: 100px;
width: var(--sheldWidth);
@ -405,14 +405,14 @@ code {
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
scrollbar-width: thin;
flex-direction: column;
z-index: 3;
z-index: 30;
}
#form_sheld {
white-space: nowrap;
width: 100%;
margin: 1px auto 0 auto;
z-index: 3;
z-index: 30;
}
#send_form {
@ -423,7 +423,7 @@ code {
margin: 0 auto 0 auto;
border: 1px solid var(--grey30a);
border-radius: 0 0 20px 20px;
border-radius: 0 0 10px 10px;
background-color: var(--SmartThemeBlurTintColor);
backdrop-filter: blur(var(--SmartThemeBlurStrength));
}
@ -500,10 +500,9 @@ code {
}
#options {
opacity: 0.0;
display: none;
z-index: 1990;
display: flex;
z-index: 100;
backdrop-filter: blur(var(--SmartThemeBlurStrength));
}
.options-content {
@ -511,12 +510,12 @@ code {
display: block;
background-color: var(--SmartThemeBlurTintColor);
border: 1px solid var(--white30a);
border-radius: 15px;
border-radius: 10px;
box-shadow: 0 0 5px black;
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength)*2));
z-index: 2000;
margin-bottom: 3px;
/* margin-bottom: 0px;*/
}
.options-content i {
@ -578,10 +577,11 @@ hr {
.options-content a,
#extensionsMenu>div {
color: var(--SmartThemeBodyColor);
padding: 12px 16px;
padding: 5px 5px;
padding-bottom: 5px;
text-decoration: none;
display: flex;
align-items: center;
/* align-items: center; */
column-gap: 10px;
cursor: pointer;
}
@ -902,6 +902,10 @@ select {
margin-top: 10px;
}
.marginTopBot5 {
margin: 5px 0;
}
#description_textarea,
#firstmessage_textarea {
height: -webkit-fill-available;
@ -933,10 +937,10 @@ select {
border: 1px solid var(--white30a);
border-radius: 10px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 7px;
width: 100%;
margin: 5px 0;
height: fit-content;
}
@ -1316,7 +1320,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
}
.alignitemsflexstart {
align-items: flex-start;
align-items: flex-start !important;
}
.gap5px {
@ -3359,7 +3363,7 @@ a {
background-color: var(--SmartThemeBlurTintColor);
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength)*2));
border: 1px solid var(--white30a);
border-radius: 15px;
border-radius: 10px;
box-shadow: 0 0 5px black;
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
}
@ -3497,7 +3501,7 @@ a {
}
#extensions_status {
margin-bottom: 10px;
/* margin-bottom: 10px; */
font-weight: 700;
}
@ -3558,6 +3562,22 @@ label[for="extensions_autoconnect"] {
color: lightgray;
}
#extensions_settings .inline-drawer-toggle.inline-drawer-header,
#extensions_settings2 .inline-drawer-toggle.inline-drawer-header {
/* background-color: var(--SmartThemeBlurTintColor); */
background-image: linear-gradient(348deg, var(--white30a)2%, var(--grey30a)10%, var(--black70a)95%, var(--SmartThemeQuoteColor)100%);
margin-bottom: 5px;
border-radius: 10px;
padding: 5px;
border: 1px solid var(--grey30);
transition: all 250ms;
}
#extensions_settings .inline-drawer-toggle.inline-drawer-header:hover,
#extensions_settings2 .inline-drawer-toggle.inline-drawer-header:hover {
filter: brightness(150%);
}
.success {
color: green;
}
@ -3707,9 +3727,10 @@ label[for="extensions_autoconnect"] {
left: 0;
right: auto;
padding: 5px;
border-radius: 0 0 20px 0;
border-radius: 10px;
box-shadow: none;
overflow: hidden;
border: 1px solid var(--grey30a);
}
.scrollableInner {
@ -3791,6 +3812,10 @@ toolcool-color-picker {
width: 50% !important;
}
.wide30p {
width: 30% !important;
}
.justifyLeft {
text-align: start;
@ -4183,6 +4208,12 @@ body.waifuMode #avatar_zoom_popup {
@media screen and (max-width: 1000px) {
#extensions_settings,
#extensions_settings2 {
width: 100% !important;
min-width: 100% !important;
}
#avatar_zoom_popup {
min-width: 100px;
min-height: 100px;
@ -4270,7 +4301,7 @@ body.waifuMode #avatar_zoom_popup {
/* ,
#world_popup */
{
max-height: calc(100svh - 45px);
max-height: calc(100svh - 40px);
width: 100% !important;
margin: 0 auto;
max-width: 100%;
@ -4462,9 +4493,16 @@ body.waifuMode #avatar_zoom_popup {
max-height: unset;
width: 100svw;
height: calc(100svh - 40px);
padding-right: max(env(safe-area-inset-right), 5px);
padding-left: max(env(safe-area-inset-left), 5px);
padding-right: max(env(safe-area-inset-right), 0px);
padding-left: max(env(safe-area-inset-left), 0px);
padding-bottom: 0;
}
body.PWA #sheld {
padding-right: max(env(safe-area-inset-right), 2px);
padding-left: max(env(safe-area-inset-left), 2px);
padding-bottom: max(env(safe-area-inset-bottom), 15px);
}
#character_popup,

290
server.js
View File

@ -3,6 +3,11 @@
const process = require('process')
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const net = require("net");
// work around a node v20 bug: https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
if (net.setDefaultAutoSelectFamily) {
net.setDefaultAutoSelectFamily(false);
}
const cliArguments = yargs(hideBin(process.argv))
.option('ssl', {
@ -99,7 +104,7 @@ client.on('error', (err) => {
console.error('An error occurred:', err);
});
const poe = require('./poe-client');
const poe = require('./src/poe-client');
let api_server = "http://0.0.0.0:5000";
let api_novelai = "https://api.novelai.net";
@ -123,23 +128,25 @@ const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const { SentencePieceProcessor, cleanText } = require("sentencepiece-js");
let spp;
let spp_llama;
let spp_nerd;
let spp_nerd_v2;
async function loadSentencepieceTokenizer() {
async function loadSentencepieceTokenizer(modelPath) {
try {
const spp = new SentencePieceProcessor();
await spp.load("src/sentencepiece/tokenizer.model");
await spp.load(modelPath);
return spp;
} catch (error) {
console.error("Sentencepiece tokenizer failed to load.");
console.error("Sentencepiece tokenizer failed to load: " + modelPath, error);
return null;
}
};
async function countTokensLlama(text) {
async function countSentencepieceTokens(spp, text) {
// Fallback to strlen estimation
if (!spp) {
return Math.ceil(v.length / 3.35);
return Math.ceil(text.length / 3.35);
}
let cleaned = cleanText(text);
@ -712,11 +719,27 @@ function readFromV2(char) {
//console.log(`Migrating field: ${charField} from ${v2Path}`);
const v2Value = _.get(char.data, v2Path);
if (_.isUndefined(v2Value)) {
console.debug(`Spec v2 data missing for field: ${charField}`);
return;
let defaultValue = undefined;
// Backfill default values for missing ST extension fields
if (v2Path === 'extensions.talkativeness') {
defaultValue = 0.5;
}
if (v2Path === 'extensions.fav') {
defaultValue = false;
}
if (!_.isUndefined(defaultValue)) {
//console.debug(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
char[charField] = defaultValue;
} else {
console.debug(`Spec v2 data missing for unknown field: ${charField}`);
return;
}
}
if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && char[charField] !== v2Value) {
console.debug(`Spec v2 data mismatch with Spec v1 for field: ${charField}`);
console.debug(`Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
}
char[charField] = v2Value;
});
@ -730,50 +753,58 @@ function charaFormatData(data) {
const _ = require('lodash');
const char = tryParse(data.json_data) || {};
// This function uses _.cond() to create a series of conditional checks that return the desired output based on the input data.
// It checks if data.alternate_greetings is an array, a string, or neither, and acts accordingly.
const getAlternateGreetings = data => _.cond([
[d => Array.isArray(d.alternate_greetings), d => d.alternate_greetings],
[d => typeof d.alternate_greetings === 'string', d => [d.alternate_greetings]],
[_.stubTrue, _.constant([])]
])(data);
// Spec V1 fields
_.set(char, 'name', data.ch_name);
_.set(char, 'description', data.description);
_.set(char, 'personality', data.personality);
_.set(char, 'scenario', data.scenario);
_.set(char, 'first_mes', data.first_mes);
_.set(char, 'mes_example', data.mes_example);
_.set(char, 'description', data.description || '');
_.set(char, 'personality', data.personality || '');
_.set(char, 'scenario', data.scenario || '');
_.set(char, 'first_mes', data.first_mes || '');
_.set(char, 'mes_example', data.mes_example || '');
// Old ST extension fields (for backward compatibility, will be deprecated)
_.set(char, 'creatorcomment', data.creator_notes);
_.set(char, 'avatar', 'none');
_.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
_.set(char, 'talkativeness', data.talkativeness);
_.set(char, 'fav', data.fav);
_.set(char, 'fav', data.fav == 'true');
_.set(char, 'create_date', humanizedISO8601DateTime());
// Spec V2 fields
_.set(char, 'spec', 'chara_card_v2');
_.set(char, 'spec_version', '2.0');
_.set(char, 'data.name', data.ch_name);
_.set(char, 'data.description', data.description);
_.set(char, 'data.personality', data.personality);
_.set(char, 'data.scenario', data.scenario);
_.set(char, 'data.first_mes', data.first_mes);
_.set(char, 'data.mes_example', data.mes_example);
_.set(char, 'data.description', data.description || '');
_.set(char, 'data.personality', data.personality || '');
_.set(char, 'data.scenario', data.scenario || '');
_.set(char, 'data.first_mes', data.first_mes || '');
_.set(char, 'data.mes_example', data.mes_example || '');
// New V2 fields
_.set(char, 'data.creator_notes', data.creator_notes);
_.set(char, 'data.system_prompt', data.system_prompt);
_.set(char, 'data.post_history_instructions', data.post_history_instructions);
_.set(char, 'data.creator_notes', data.creator_notes || '');
_.set(char, 'data.system_prompt', data.system_prompt || '');
_.set(char, 'data.post_history_instructions', data.post_history_instructions || '');
_.set(char, 'data.tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : []);
_.set(char, 'data.creator', data.creator);
_.set(char, 'data.character_version', data.character_version);
_.set(char, 'data.alternative_greetings', data.alternate_greetings);
_.set(char, 'data.creator', data.creator || '');
_.set(char, 'data.character_version', data.character_version || '');
_.set(char, 'data.alternate_greetings', getAlternateGreetings(data));
// ST extension fields to V2 object
_.set(char, 'data.extensions.talkativeness', data.talkativeness);
_.set(char, 'data.extensions.fav', data.fav);
_.set(char, 'data.extensions.fav', data.fav == 'true');
//_.set(char, 'data.extensions.create_date', humanizedISO8601DateTime());
//_.set(char, 'data.extensions.avatar', 'none');
//_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
// TODO: Character book
_.set(char, 'data.character_book', undefined);
_//.set(char, 'data.character_book', undefined);
return char;
}
@ -840,10 +871,12 @@ app.post("/renamecharacter", jsonParser, async function (request, response) {
const newChatsPath = path.join(chatsPath, newInternalName);
try {
const _ = require('lodash');
// Read old file, replace name int it
const rawOldData = await charaRead(oldAvatarPath);
const oldData = json5.parse(rawOldData);
oldData['name'] = newName;
const oldData = getCharaCardV2(json5.parse(rawOldData));
_.set(oldData, 'data.name', newName);
_.set(oldData, 'name', newName);
const newData = JSON.stringify(oldData);
// Write data to new location
@ -1002,14 +1035,7 @@ app.post("/getcharacters", jsonParser, function (request, response) {
for (const item of pngFiles) {
try {
var img_data = await charaRead(charactersPath + item);
let jsonObject = json5.parse(img_data);
if (jsonObject.spec === undefined) {
jsonObject = convertToV2(jsonObject);
} else {
jsonObject = readFromV2(jsonObject);
}
let jsonObject = getCharaCardV2(json5.parse(img_data));
jsonObject.avatar = item;
characters[i] = {};
characters[i] = jsonObject;
@ -1183,6 +1209,15 @@ app.post("/savesettings", jsonParser, function (request, response) {
});
});
function getCharaCardV2(jsonObject) {
if (jsonObject.spec === undefined) {
jsonObject = convertToV2(jsonObject);
} else {
jsonObject = readFromV2(jsonObject);
}
return jsonObject;
}
function readAndParseFromDirectory(directoryPath, fileExtension = '.json') {
const files = fs
.readdirSync(directoryPath)
@ -1569,9 +1604,17 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
console.log(err);
response.send({ error: true });
}
const jsonData = json5.parse(data);
if (jsonData.name !== undefined) {
let = jsonData = json5.parse(data);
if (jsonData.spec !== undefined) {
console.log('importing from v2 json');
jsonData = readFromV2(jsonData);
png_name = getPngName(jsonData.data?.name || jsonData.name);
let char = JSON.stringify(jsonData);
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.name !== undefined) {
console.log('importing from v1 json');
jsonData.name = sanitize(jsonData.name);
png_name = getPngName(jsonData.name);
@ -1581,7 +1624,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
"creatorcomment": jsonData.creatorcomment ?? '',
"personality": jsonData.personality ?? '',
"first_mes": jsonData.first_mes ?? '',
"avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(),
"avatar": 'none',
"chat": jsonData.name + " - " + humanizedISO8601DateTime(),
"mes_example": jsonData.mes_example ?? '',
"scenario": jsonData.scenario ?? '',
"create_date": humanizedISO8601DateTime(),
@ -1591,6 +1635,7 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
char = JSON.stringify(char);
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
console.log('importing from gradio json');
jsonData.char_name = sanitize(jsonData.char_name);
png_name = getPngName(jsonData.char_name);
@ -1619,7 +1664,9 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
try {
var img_data = await charaRead(uploadPath, format);
let jsonData = json5.parse(img_data);
jsonData.name = sanitize(jsonData.name);
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
png_name = getPngName(jsonData.name);
if (format == 'webp') {
try {
@ -1633,9 +1680,13 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
}
}
png_name = getPngName(jsonData.name);
if (jsonData.name !== undefined) {
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');
jsonData = readFromV2(jsonData);
let char = JSON.stringify(jsonData);
charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.name !== undefined) {
console.log('Found a v1 character file.');
let char = {
"name": jsonData.name,
"description": jsonData.description ?? '',
@ -1652,6 +1703,9 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
char = convertToV2(char);
char = JSON.stringify(char);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
} else {
console.log('Unknown character card format');
response.send({ error: true });
}
} catch (err) {
console.log(err);
@ -1759,7 +1813,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
case 'json': {
try {
let json = await charaRead(filename);
let jsonObject = json5.parse(json);
let jsonObject = getCharaCardV2(json5.parse(json));
return response.type('json').send(jsonObject)
}
catch {
@ -2172,24 +2226,48 @@ app.post('/deletegroup', jsonParser, async (request, response) => {
const id = request.body.id;
const pathToGroup = path.join(directories.groups, sanitize(`${id}.json`));
const pathToChat = path.join(directories.groupChats, sanitize(`${id}.jsonl`));
try {
// Delete group chats
const group = json5.parse(fs.readFileSync(pathToGroup));
if (group && Array.isArray(group.chats)) {
for (const chat of group.chats) {
console.log('Deleting group chat', chat);
const pathToFile = path.join(directories.groupChats, `${id}.jsonl`);
if (fs.existsSync(pathToFile)) {
fs.rmSync(pathToFile);
}
}
}
} catch (error) {
console.error('Could not delete group chats. Clean them up manually.', error);
}
if (fs.existsSync(pathToGroup)) {
fs.rmSync(pathToGroup);
}
if (fs.existsSync(pathToChat)) {
fs.rmSync(pathToChat);
}
return response.send({ ok: true });
});
const POE_DEFAULT_BOT = 'a2';
const poeClientCache = {};
async function getPoeClient(token, useCache = false) {
let client = new poe.Client(false, useCache);
await client.init(token);
let client;
if (useCache && poeClientCache[token]) {
client = poeClientCache[token];
}
else {
client = new poe.Client(true, useCache);
await client.init(token);
}
poeClientCache[token] = client;
return client;
}
@ -2201,9 +2279,9 @@ app.post('/status_poe', jsonParser, async (request, response) => {
}
try {
const client = await getPoeClient(token);
const client = await getPoeClient(token, false);
const botNames = client.get_bot_names();
client.disconnect_ws();
//client.disconnect_ws();
return response.send({ 'bot_names': botNames });
}
@ -2231,7 +2309,7 @@ app.post('/purge_poe', jsonParser, async (request, response) => {
else {
await client.send_chat_break(bot);
}
client.disconnect_ws();
//client.disconnect_ws();
return response.send({ "ok": true });
}
@ -2253,12 +2331,13 @@ app.post('/generate_poe', jsonParser, async (request, response) => {
}
let isGenerationStopped = false;
const abortController = new AbortController();
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
isGenerationStopped = true;
if (client) {
client.abortController.abort();
abortController.abort();
}
});
const prompt = request.body.prompt;
@ -2284,7 +2363,7 @@ app.post('/generate_poe', jsonParser, async (request, response) => {
});
let reply = '';
for await (const mes of client.send_message(bot, prompt)) {
for await (const mes of client.send_message(bot, prompt, false, 30, abortController.signal)) {
if (isGenerationStopped) {
console.error('Streaming stopped by user. Closing websocket...');
break;
@ -2300,22 +2379,22 @@ app.post('/generate_poe', jsonParser, async (request, response) => {
console.error(err);
}
finally {
client.disconnect_ws();
//client.disconnect_ws();
response.end();
}
}
else {
try {
let reply;
for await (const mes of client.send_message(bot, prompt)) {
for await (const mes of client.send_message(bot, prompt, false, 30, abortController.signal)) {
reply = mes.text;
}
console.log(reply);
client.disconnect_ws();
//client.disconnect_ws();
return response.send({ 'reply': reply });
}
catch {
client.disconnect_ws();
//client.disconnect_ws();
return response.sendStatus(500);
}
}
@ -2761,14 +2840,22 @@ app.post("/savepreset_openai", jsonParser, function (request, response) {
return response.send({ name });
});
app.post("/tokenize_llama", jsonParser, async function (request, response) {
if (!request.body) {
return response.sendStatus(400);
}
function createTokenizationHandler(getTokenizerFn) {
return async function (request, response) {
if (!request.body) {
return response.sendStatus(400);
}
const count = await countTokensLlama(request.body.text);
return response.send({ count });
});
const text = request.body.text || '';
const tokenizer = getTokenizerFn();
const count = await countSentencepieceTokens(tokenizer, text);
return response.send({ count });
};
}
app.post("/tokenize_llama", jsonParser, createTokenizationHandler(() => spp_llama));
app.post("/tokenize_nerdstash", jsonParser, createTokenizationHandler(() => spp_nerd));
app.post("/tokenize_nerdstash_v2", jsonParser, createTokenizationHandler(() => spp_nerd_v2));
// ** REST CLIENT ASYNC WRAPPERS **
@ -2784,7 +2871,8 @@ function putAsync(url, args) {
}
async function postAsync(url, args) {
const response = await fetch(url, { method: 'POST', ...args });
const fetch = require('node-fetch').default;
const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
if (response.ok) {
const data = await response.json();
@ -2826,7 +2914,11 @@ const setupTasks = async function () {
// Colab users could run the embedded tool
if (!is_colab) await convertWebp();
spp = await loadSentencepieceTokenizer();
[spp_llama, spp_nerd, spp_nerd_v2] = await Promise.all([
loadSentencepieceTokenizer('src/sentencepiece/tokenizer.model'),
loadSentencepieceTokenizer('src/sentencepiece/nerdstash.model'),
loadSentencepieceTokenizer('src/sentencepiece/nerdstash_v2.model'),
]);
console.log('Launching...');
@ -3162,6 +3254,40 @@ app.post('/google_translate', jsonParser, async (request, response) => {
});
});
app.post('/novel_tts', jsonParser, async (request, response) => {
const token = readSecret(SECRET_KEYS.NOVEL);
if (!token) {
return response.sendStatus(401);
}
const text = request.body.text;
const voice = request.body.voice;
if (!text || !voice) {
return response.sendStatus(400);
}
try {
const fetch = require('node-fetch').default;
const url = `${api_novelai}/ai/generate-voice?text=${encodeURIComponent(text)}&voice=-1&seed=${encodeURIComponent(voice)}&opus=false&version=v2`;
const result = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'audio/webm' } });
if (!result.ok) {
return response.sendStatus(result.status);
}
const chunks = await readAllChunks(result.body);
const buffer = Buffer.concat(chunks);
response.setHeader('Content-Type', 'audio/webm');
return response.send(buffer);
}
catch (error) {
console.error(error);
return response.sendStatus(500);
}
});
app.post('/delete_sprite', jsonParser, async (request, response) => {
const label = request.body.label;
const name = request.body.name;
@ -3308,6 +3434,26 @@ function readSecret(key) {
return secrets[key];
}
async function readAllChunks(readableStream) {
return new Promise((resolve, reject) => {
// Consume the readable stream
const chunks = [];
readableStream.on('data', (chunk) => {
chunks.push(chunk);
});
readableStream.on('end', () => {
console.log('Finished reading the stream.');
resolve(chunks);
});
readableStream.on('error', (error) => {
console.error('Error while reading the stream:', error);
reject();
});
});
}
async function getImageBuffers(zipFilePath) {
return new Promise((resolve, reject) => {
// Check if the zip file exists

View File

@ -32,6 +32,7 @@ let queries = {};
const cached_bots = {};
const logger = console;
const delay = ms => new Promise(res => setTimeout(res, ms));
const user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36";
@ -246,6 +247,7 @@ class Client {
settings_url = "https://poe.com/api/settings";
formkey = "";
token = "";
next_data = {};
bots = {};
active_messages = {};
@ -259,16 +261,23 @@ class Client {
constructor(auto_reconnect = false, use_cached_bots = false) {
this.auto_reconnect = auto_reconnect;
this.use_cached_bots = use_cached_bots;
this.abortController = new AbortController();
}
async reconnect() {
if (!this.ws_connected) {
console.log("WebSocket died. Reconnecting...");
this.disconnect_ws();
await this.init(this.token, this.proxy);
}
}
async init(token, proxy = null) {
this.token = token;
this.proxy = proxy;
this.session = axios.default.create({
timeout: 60000,
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
signal: this.abortController.signal,
});
if (proxy) {
this.session.defaults.proxy = {
@ -285,18 +294,17 @@ class Client {
"Cookie": cookies,
};
this.session.defaults.headers.common = this.headers;
this.next_data = await this.get_next_data();
this.channel = await this.get_channel_data();
[this.next_data, this.channel] = await Promise.all([this.get_next_data(), this.get_channel_data()]);
this.bots = await this.get_bots();
this.bot_names = this.get_bot_names();
this.ws_domain = `tch${Math.floor(Math.random() * 1e6)}`;
this.gql_headers = {
"poe-formkey": this.formkey,
"poe-tchannel": this.channel["channel"],
...this.headers,
};
await this.connect_ws();
await this.subscribe();
await this.connect_ws();
console.log('Client initialized.');
}
async get_next_data() {
@ -321,28 +329,37 @@ class Client {
const botList = viewer.viewerBotList;
const retries = 2;
const bots = {};
const promises = [];
for (const bot of botList.filter(x => x.deletionState == 'not_deleted')) {
try {
const url = `https://poe.com/_next/data/${this.next_data.buildId}/${bot.displayName}.json`;
let r;
const promise = new Promise(async (resolve, reject) => {
try {
const url = `https://poe.com/_next/data/${this.next_data.buildId}/${bot.displayName}.json`;
let r;
if (this.use_cached_bots && cached_bots[url]) {
r = cached_bots[url];
}
else {
logger.info(`Downloading ${url}`);
r = await request_with_retries(() => this.session.get(url), retries);
cached_bots[url] = r;
}
if (this.use_cached_bots && cached_bots[url]) {
r = cached_bots[url];
}
else {
logger.info(`Downloading ${url}`);
r = await request_with_retries(() => this.session.get(url), retries);
cached_bots[url] = r;
}
const chatData = r.data.pageProps.payload.chatOfBotDisplayName;
bots[chatData.defaultBotObject.nickname] = chatData;
}
catch {
console.log(`Could not load bot: ${bot.displayName}`);
}
const chatData = r.data.pageProps.payload.chatOfBotDisplayName;
bots[chatData.defaultBotObject.nickname] = chatData;
resolve();
}
catch {
console.log(`Could not load bot: ${bot.displayName}`);
reject();
}
});
promises.push(promise);
}
await Promise.allSettled(promises);
return bots;
}
@ -380,9 +397,9 @@ class Client {
_headers['poe-tag-id'] = md5()(scramblePayload + this.formkey + "WpuLMiXEKKE98j56k");
_headers['poe-formkey'] = this.formkey;
const r = await request_with_retries(() => this.session.post(this.gql_url, payload, { headers: this.gql_headers }));
if (!r.data.data) {
logger.warn(`${queryName} returned an error: ${data.errors[0].message} | Retrying (${i + 1}/20)`);
await new Promise((resolve) => setTimeout(resolve, 2000));
if (!(r?.data?.data)) {
logger.warn(`${queryName} returned an error | Retrying (${i + 1}/20)`);
await delay(2000);
continue;
}
@ -392,6 +409,29 @@ class Client {
throw new Error(`${queryName} failed too many times.`);
}
async ws_ping() {
const pongPromise = new Promise((resolve) => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
}
this.ws.once('pong', () => {
resolve('ok');
});
});
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), 5000));
const result = await Promise.race([pongPromise, timeoutPromise]);
if (result == 'ok') {
return true;
}
else {
logger.warn('Websocket ping timed out.');
this.ws_connected = false;
return false;
}
}
async subscribe() {
logger.info("Subscribing to mutations")
await this.send_query("SubscriptionsMutation", {
@ -439,10 +479,11 @@ class Client {
}
async connect_ws() {
this.ws_domain = `tch${Math.floor(Math.random() * 1e6)}`;
this.ws_connected = false;
this.ws_run_thread();
while (!this.ws_connected) {
await new Promise(resolve => setTimeout(() => { resolve() }, 10));
await delay(10);
}
}
@ -460,10 +501,6 @@ class Client {
on_ws_error(ws, error) {
logger.warn(`Websocket returned error: ${error}`);
this.disconnect_ws();
if (this.auto_reconnect) {
this.connect_ws();
}
}
async on_message(ws, msg) {
@ -510,10 +547,16 @@ class Client {
}
}
async *send_message(chatbot, message, with_chat_break = false, timeout = 20) {
async *send_message(chatbot, message, with_chat_break = false, timeout = 30, signal = null) {
await this.ws_ping();
if (this.auto_reconnect) {
await this.reconnect();
}
//if there is another active message, wait until it has finished sending
while (Object.values(this.active_messages).includes(null)) {
await new Promise(resolve => setTimeout(resolve, 10));
await delay(10);
}
//null indicates that a message is still in progress
@ -551,11 +594,18 @@ class Client {
let messageId;
while (true) {
try {
this.abortController.signal.throwIfAborted();
if (signal instanceof AbortSignal) {
signal.throwIfAborted();
}
if (timeout == 0) {
throw new Error("Response timed out.");
}
const message = this.message_queues[humanMessageId].shift();
if (!message) {
await new Promise(resolve => setTimeout(() => resolve(), 1000));
timeout -= 1;
await delay(1000);
continue;
//throw new Error("Queue is empty");
}

Binary file not shown.

Binary file not shown.