mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
(beta) Message translate plugin
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"device-detector-js": "^3.0.3",
|
"device-detector-js": "^3.0.3",
|
||||||
"exifreader": "^4.12.0",
|
"exifreader": "^4.12.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"google-translate-api-browser": "^3.0.1",
|
||||||
"gpt3-tokenizer": "^1.1.5",
|
"gpt3-tokenizer": "^1.1.5",
|
||||||
"ip-matching": "^2.1.2",
|
"ip-matching": "^2.1.2",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
@ -1525,6 +1526,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/google-translate-api-browser": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/google-translate-api-browser/-/google-translate-api-browser-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-KTLodkyGBWMK9IW6QIeJ2zCuju4Z0CLpbkADKo+yLhbSTD4l+CXXpQ/xaynGVAzeBezzJG6qn8MLeqOq3SmW0A=="
|
||||||
|
},
|
||||||
"node_modules/gpt3-tokenizer": {
|
"node_modules/gpt3-tokenizer": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"device-detector-js": "^3.0.3",
|
"device-detector-js": "^3.0.3",
|
||||||
"exifreader": "^4.12.0",
|
"exifreader": "^4.12.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"google-translate-api-browser": "^3.0.1",
|
||||||
"gpt3-tokenizer": "^1.1.5",
|
"gpt3-tokenizer": "^1.1.5",
|
||||||
"ip-matching": "^2.1.2",
|
"ip-matching": "^2.1.2",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
|
@ -2528,6 +2528,7 @@
|
|||||||
<span class="name_text">${characterName}</span>
|
<span class="name_text">${characterName}</span>
|
||||||
|
|
||||||
<div class="mes_buttons">
|
<div class="mes_buttons">
|
||||||
|
<div title="Translate message" class="mes_translate fa-solid fa-language"></div>
|
||||||
<div title="Open bookmark chat" class="mes_bookmark fa-solid fa-bookmark"></div>
|
<div title="Open bookmark chat" class="mes_bookmark fa-solid fa-bookmark"></div>
|
||||||
<div title="Generate Image" class="sd_message_gen fa-solid fa-paintbrush"></div>
|
<div title="Generate Image" class="sd_message_gen fa-solid fa-paintbrush"></div>
|
||||||
<div title="Narrate" class="mes_narrate fa-solid fa-bullhorn"></div>
|
<div title="Narrate" class="mes_narrate fa-solid fa-bullhorn"></div>
|
||||||
|
@ -1135,6 +1135,10 @@ function addCopyToCodeBlocks(messageElement) {
|
|||||||
function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true } = {}) {
|
function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true } = {}) {
|
||||||
var messageText = mes["mes"];
|
var messageText = mes["mes"];
|
||||||
|
|
||||||
|
if (mes?.extra?.display_text) {
|
||||||
|
messageText = mes.extra.display_text;
|
||||||
|
}
|
||||||
|
|
||||||
if (mes.name === name1) {
|
if (mes.name === name1) {
|
||||||
var characterName = name1; //set to user's name by default
|
var characterName = name1; //set to user's name by default
|
||||||
} else { var characterName = mes.name }
|
} else { var characterName = mes.name }
|
||||||
@ -4803,6 +4807,16 @@ function swipe_left() { // when we swipe left..but no generation.
|
|||||||
this_mes_div.css('height', this_mes_div_height);
|
this_mes_div.css('height', this_mes_div_height);
|
||||||
const this_mes_block_height = this_mes_block[0].scrollHeight;
|
const this_mes_block_height = this_mes_block[0].scrollHeight;
|
||||||
chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']];
|
chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']];
|
||||||
|
if (chat[chat.length - 1].extra) {
|
||||||
|
// if message has memory attached - remove it to allow regen
|
||||||
|
if (chat[chat.length - 1].extra.memory) {
|
||||||
|
delete chat[chat.length - 1].extra.memory;
|
||||||
|
}
|
||||||
|
// ditto for display text
|
||||||
|
if (chat[chat.length - 1].extra.display_text) {
|
||||||
|
delete chat[chat.length - 1].extra.display_text;
|
||||||
|
}
|
||||||
|
}
|
||||||
$(this).parent().children('.mes_block').transition({
|
$(this).parent().children('.mes_block').transition({
|
||||||
x: swipe_range,
|
x: swipe_range,
|
||||||
duration: swipe_duration,
|
duration: swipe_duration,
|
||||||
@ -4901,9 +4915,15 @@ const swipe_right = () => {
|
|||||||
chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat
|
chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat
|
||||||
}
|
}
|
||||||
chat[chat.length - 1]['swipe_id']++; //make new slot in array
|
chat[chat.length - 1]['swipe_id']++; //make new slot in array
|
||||||
// if message has memory attached - remove it to allow regen
|
if (chat[chat.length - 1].extra) {
|
||||||
if (chat[chat.length - 1].extra && chat[chat.length - 1].extra.memory) {
|
// if message has memory attached - remove it to allow regen
|
||||||
delete chat[chat.length - 1].extra.memory;
|
if ( chat[chat.length - 1].extra.memory) {
|
||||||
|
delete chat[chat.length - 1].extra.memory;
|
||||||
|
}
|
||||||
|
// ditto for display text
|
||||||
|
if (chat[chat.length - 1].extra.display_text) {
|
||||||
|
delete chat[chat.length - 1].extra.display_text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//console.log(chat[chat.length-1]['swipes']);
|
//console.log(chat[chat.length-1]['swipes']);
|
||||||
if (parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { //if swipe id of last message is the same as the length of the 'swipes' array
|
if (parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { //if swipe id of last message is the same as the length of the 'swipes' array
|
||||||
|
@ -28,6 +28,7 @@ const extension_settings = {
|
|||||||
tts: {},
|
tts: {},
|
||||||
sd: {},
|
sd: {},
|
||||||
chromadb: {},
|
chromadb: {},
|
||||||
|
translate: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let modules = [];
|
let modules = [];
|
||||||
@ -342,4 +343,4 @@ $(document).ready(async function () {
|
|||||||
$("#extensions_details").on('click', showExtensionsDetails);
|
$("#extensions_details").on('click', showExtensionsDetails);
|
||||||
$(document).on('click', '.disable_extension', onDisableExtensionClick);
|
$(document).on('click', '.disable_extension', onDisableExtensionClick);
|
||||||
$(document).on('click', '.enable_extension', onEnableExtensionClick);
|
$(document).on('click', '.enable_extension', onEnableExtensionClick);
|
||||||
});
|
});
|
||||||
|
248
public/scripts/extensions/translate/index.js
Normal file
248
public/scripts/extensions/translate/index.js
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import { eventSource, event_types, getRequestHeaders, messageFormatting, saveSettingsDebounced } from "../../../script.js";
|
||||||
|
import { extension_settings, getContext } from "../../extensions.js";
|
||||||
|
|
||||||
|
const defaultSettings = {
|
||||||
|
target_language: 'en',
|
||||||
|
provider: 'google',
|
||||||
|
auto: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageCodes = {
|
||||||
|
'Afrikaans': 'af',
|
||||||
|
'Albanian': 'sq',
|
||||||
|
'Amharic': 'am',
|
||||||
|
'Arabic': 'ar',
|
||||||
|
'Armenian': 'hy',
|
||||||
|
'Azerbaijani': 'az',
|
||||||
|
'Basque': 'eu',
|
||||||
|
'Belarusian': 'be',
|
||||||
|
'Bengali': 'bn',
|
||||||
|
'Bosnian': 'bs',
|
||||||
|
'Bulgarian': 'bg',
|
||||||
|
'Catalan': 'ca',
|
||||||
|
'Cebuano': 'ceb',
|
||||||
|
'Chinese (Simplified)': 'zh-CN',
|
||||||
|
'Chinese (Traditional)': 'zh-TW',
|
||||||
|
'Corsican': 'co',
|
||||||
|
'Croatian': 'hr',
|
||||||
|
'Czech': 'cs',
|
||||||
|
'Danish': 'da',
|
||||||
|
'Dutch': 'nl',
|
||||||
|
'English': 'en',
|
||||||
|
'Esperanto': 'eo',
|
||||||
|
'Estonian': 'et',
|
||||||
|
'Finnish': 'fi',
|
||||||
|
'French': 'fr',
|
||||||
|
'Frisian': 'fy',
|
||||||
|
'Galician': 'gl',
|
||||||
|
'Georgian': 'ka',
|
||||||
|
'German': 'de',
|
||||||
|
'Greek': 'el',
|
||||||
|
'Gujarati': 'gu',
|
||||||
|
'Haitian Creole': 'ht',
|
||||||
|
'Hausa': 'ha',
|
||||||
|
'Hawaiian': 'haw',
|
||||||
|
'Hebrew': 'iw',
|
||||||
|
'Hindi': 'hi',
|
||||||
|
'Hmong': 'hmn',
|
||||||
|
'Hungarian': 'hu',
|
||||||
|
'Icelandic': 'is',
|
||||||
|
'Igbo': 'ig',
|
||||||
|
'Indonesian': 'id',
|
||||||
|
'Irish': 'ga',
|
||||||
|
'Italian': 'it',
|
||||||
|
'Japanese': 'ja',
|
||||||
|
'Javanese': 'jw',
|
||||||
|
'Kannada': 'kn',
|
||||||
|
'Kazakh': 'kk',
|
||||||
|
'Khmer': 'km',
|
||||||
|
'Korean': 'ko',
|
||||||
|
'Kurdish': 'ku',
|
||||||
|
'Kyrgyz': 'ky',
|
||||||
|
'Lao': 'lo',
|
||||||
|
'Latin': 'la',
|
||||||
|
'Latvian': 'lv',
|
||||||
|
'Lithuanian': 'lt',
|
||||||
|
'Luxembourgish': 'lb',
|
||||||
|
'Macedonian': 'mk',
|
||||||
|
'Malagasy': 'mg',
|
||||||
|
'Malay': 'ms',
|
||||||
|
'Malayalam': 'ml',
|
||||||
|
'Maltese': 'mt',
|
||||||
|
'Maori': 'mi',
|
||||||
|
'Marathi': 'mr',
|
||||||
|
'Mongolian': 'mn',
|
||||||
|
'Myanmar (Burmese)': 'my',
|
||||||
|
'Nepali': 'ne',
|
||||||
|
'Norwegian': 'no',
|
||||||
|
'Nyanja (Chichewa)': 'ny',
|
||||||
|
'Pashto': 'ps',
|
||||||
|
'Persian': 'fa',
|
||||||
|
'Polish': 'pl',
|
||||||
|
'Portuguese (Portugal, Brazil)': 'pt',
|
||||||
|
'Punjabi': 'pa',
|
||||||
|
'Romanian': 'ro',
|
||||||
|
'Russian': 'ru',
|
||||||
|
'Samoan': 'sm',
|
||||||
|
'Scots Gaelic': 'gd',
|
||||||
|
'Serbian': 'sr',
|
||||||
|
'Sesotho': 'st',
|
||||||
|
'Shona': 'sn',
|
||||||
|
'Sindhi': 'sd',
|
||||||
|
'Sinhala (Sinhalese)': 'si',
|
||||||
|
'Slovak': 'sk',
|
||||||
|
'Slovenian': 'sl',
|
||||||
|
'Somali': 'so',
|
||||||
|
'Spanish': 'es',
|
||||||
|
'Sundanese': 'su',
|
||||||
|
'Swahili': 'sw',
|
||||||
|
'Swedish': 'sv',
|
||||||
|
'Tagalog (Filipino)': 'tl',
|
||||||
|
'Tajik': 'tg',
|
||||||
|
'Tamil': 'ta',
|
||||||
|
'Telugu': 'te',
|
||||||
|
'Thai': 'th',
|
||||||
|
'Turkish': 'tr',
|
||||||
|
'Ukrainian': 'uk',
|
||||||
|
'Urdu': 'ur',
|
||||||
|
'Uzbek': 'uz',
|
||||||
|
'Vietnamese': 'vi',
|
||||||
|
'Welsh': 'cy',
|
||||||
|
'Xhosa': 'xh',
|
||||||
|
'Yiddish': 'yi',
|
||||||
|
'Yoruba': 'yo',
|
||||||
|
'Zulu': 'zu',
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
if (Object.keys(extension_settings.translate).length === 0) {
|
||||||
|
Object.assign(extension_settings.translate, defaultSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(`#translation_provider option[value="${extension_settings.translate.provider}"]`).attr('selected', true);
|
||||||
|
$(`#translation_target_language option[value="${extension_settings.translate.target_language}"]`).attr('selected', true);
|
||||||
|
$('#translation_auto').prop('checked', extension_settings.translate.auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translateIncomingMessage(messageId) {
|
||||||
|
const context = getContext();
|
||||||
|
const message = context.chat[messageId];
|
||||||
|
|
||||||
|
if (typeof message.extra !== 'object') {
|
||||||
|
message.extra = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// New swipe is being generated. Don't translate that
|
||||||
|
if ($(`#chat .mes[mesid="${messageId}"] .mes_text`).text() == '...') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translation = await translate(message.mes);
|
||||||
|
message.extra.display_text = translation;
|
||||||
|
|
||||||
|
$(`#chat .mes[mesid="${messageId}"] .mes_text`).html(messageFormatting(translation, message.name, message.is_system, message.is_user));
|
||||||
|
|
||||||
|
context.saveChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translateProviderGoogle(text) {
|
||||||
|
const response = await fetch('/google_translate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({ text: text, lang: extension_settings.translate.target_language }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.text();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translate(text) {
|
||||||
|
try {
|
||||||
|
switch (extension_settings.translate.provider) {
|
||||||
|
case 'google':
|
||||||
|
return await translateProviderGoogle(text);
|
||||||
|
default:
|
||||||
|
console.error('Unknown translation provider', extension_settings.translate.provider);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toastr.error('Failed to translate message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translateOutgoingMessage(messageId) {
|
||||||
|
alert('translateOutgoingMessage', messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery(() => {
|
||||||
|
const html = `
|
||||||
|
<div class="translation_settings">
|
||||||
|
<div class="inline-drawer">
|
||||||
|
<div class="inline-drawer-toggle inline-drawer-header">
|
||||||
|
<b>Chat Translation</b>
|
||||||
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-drawer-content">
|
||||||
|
<label for="translation_auto" class="checkbox_label">
|
||||||
|
<input type="checkbox" id="translation_auto" />
|
||||||
|
Auto-mode
|
||||||
|
</label>
|
||||||
|
<label for="translation_provider">Provider</label>
|
||||||
|
<select id="translation_provider" name="provider">
|
||||||
|
<option value="google">Google</option>
|
||||||
|
<select>
|
||||||
|
<label for="translation_target_language">Target Language</label>
|
||||||
|
<select id="translation_target_language" name="target_language"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
$('#extensions_settings').append(html);
|
||||||
|
for (const [key, value] of Object.entries(languageCodes)) {
|
||||||
|
$('#translation_target_language').append(`<option value="${value}">${key}</option>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
eventSource.on(event_types.MESSAGE_RECEIVED, async (messageId) => {
|
||||||
|
if (!extension_settings.translate.auto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await translateIncomingMessage(messageId);
|
||||||
|
});
|
||||||
|
eventSource.on(event_types.MESSAGE_SWIPED, async (messageId) => {
|
||||||
|
if (!extension_settings.translate.auto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await translateIncomingMessage(messageId);
|
||||||
|
});
|
||||||
|
eventSource.on(event_types.MESSAGE_SENT, async (messageId) => {
|
||||||
|
if (!extension_settings.translate.auto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await translateOutgoingMessage(messageId);
|
||||||
|
});
|
||||||
|
$('#translation_auto').on('input', (event) => {
|
||||||
|
extension_settings.translate.auto = event.target.checked;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$('#translation_provider').on('change', (event) => {
|
||||||
|
extension_settings.translate.provider = event.target.value;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$('#translation_target_language').on('change', (event) => {
|
||||||
|
extension_settings.translate.target_language = event.target.value;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$(document).on('click', '.mes_translate', function () {
|
||||||
|
const messageId = $(this).closest('.mes').attr('mesid');
|
||||||
|
translateIncomingMessage(messageId);
|
||||||
|
});
|
||||||
|
document.body.classList.add('translate');
|
||||||
|
});
|
11
public/scripts/extensions/translate/manifest.json
Normal file
11
public/scripts/extensions/translate/manifest.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"display_name": "Chat Translation",
|
||||||
|
"loading_order": 10,
|
||||||
|
"requires": [],
|
||||||
|
"optional": [],
|
||||||
|
"js": "index.js",
|
||||||
|
"css": "style.css",
|
||||||
|
"author": "Cohee#1207",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||||
|
}
|
0
public/scripts/extensions/translate/style.css
Normal file
0
public/scripts/extensions/translate/style.css
Normal file
@ -233,6 +233,7 @@ table.responsiveTable {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mes_translate,
|
||||||
.sd_message_gen,
|
.sd_message_gen,
|
||||||
.mes_narrate,
|
.mes_narrate,
|
||||||
body.tts .mes[is_user="true"] .mes_narrate,
|
body.tts .mes[is_user="true"] .mes_narrate,
|
||||||
@ -266,6 +267,7 @@ body.tts .mes[is_system="true"] .mes_narrate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.sd .sd_message_gen,
|
body.sd .sd_message_gen,
|
||||||
|
body.translate .mes_translate,
|
||||||
body.tts .mes_narrate {
|
body.tts .mes_narrate {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
33
server.js
33
server.js
@ -2975,6 +2975,39 @@ app.post('/horde_generateimage', jsonParser, async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/google_translate', jsonParser, async (request, response) => {
|
||||||
|
const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const text = request.body.text;
|
||||||
|
const lang = request.body.lang;
|
||||||
|
|
||||||
|
if (!text || !lang) {
|
||||||
|
return response.sendStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Input text: ' + text);
|
||||||
|
|
||||||
|
const url = generateRequestUrl(text, { to: lang });
|
||||||
|
|
||||||
|
https.get(url, (resp) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
resp.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
resp.on('end', () => {
|
||||||
|
const result = normaliseResponse(JSON.parse(data));
|
||||||
|
console.log('Translated text: ' + result.text);
|
||||||
|
return response.send(result.text);
|
||||||
|
});
|
||||||
|
}).on("error", (err) => {
|
||||||
|
console.log("Translation error: " + err.message);
|
||||||
|
return response.sendStatus(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function writeSecret(key, value) {
|
function writeSecret(key, value) {
|
||||||
if (!fs.existsSync(SECRETS_FILE)) {
|
if (!fs.existsSync(SECRETS_FILE)) {
|
||||||
const emptyFile = JSON.stringify({});
|
const emptyFile = JSON.stringify({});
|
||||||
|
Reference in New Issue
Block a user