diff --git a/package-lock.json b/package-lock.json
index 101fa744d..47436a294 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"device-detector-js": "^3.0.3",
"exifreader": "^4.12.0",
"express": "^4.18.2",
+ "google-translate-api-browser": "^3.0.1",
"gpt3-tokenizer": "^1.1.5",
"ip-matching": "^2.1.2",
"ipaddr.js": "^2.0.1",
@@ -1525,6 +1526,11 @@
"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": {
"version": "1.1.5",
"license": "MIT",
diff --git a/package.json b/package.json
index 98dd786ed..d30048eda 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"device-detector-js": "^3.0.3",
"exifreader": "^4.12.0",
"express": "^4.18.2",
+ "google-translate-api-browser": "^3.0.1",
"gpt3-tokenizer": "^1.1.5",
"ip-matching": "^2.1.2",
"ipaddr.js": "^2.0.1",
diff --git a/public/index.html b/public/index.html
index 0c93297e7..4ab0b1a28 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2528,6 +2528,7 @@
${characterName}
+
diff --git a/public/script.js b/public/script.js
index 5c3c6249d..88037633e 100644
--- a/public/script.js
+++ b/public/script.js
@@ -1135,6 +1135,10 @@ function addCopyToCodeBlocks(messageElement) {
function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true } = {}) {
var messageText = mes["mes"];
+ if (mes?.extra?.display_text) {
+ messageText = mes.extra.display_text;
+ }
+
if (mes.name === name1) {
var characterName = name1; //set to user's name by default
} 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);
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']];
+ 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({
x: swipe_range,
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]['swipe_id']++; //make new slot in array
- // if message has memory attached - remove it to allow regen
- if (chat[chat.length - 1].extra && chat[chat.length - 1].extra.memory) {
- delete chat[chat.length - 1].extra.memory;
+ 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;
+ }
}
//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
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index 2ce4d4b4c..a762ff366 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -28,6 +28,7 @@ const extension_settings = {
tts: {},
sd: {},
chromadb: {},
+ translate: {},
};
let modules = [];
@@ -342,4 +343,4 @@ $(document).ready(async function () {
$("#extensions_details").on('click', showExtensionsDetails);
$(document).on('click', '.disable_extension', onDisableExtensionClick);
$(document).on('click', '.enable_extension', onEnableExtensionClick);
-});
\ No newline at end of file
+});
diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js
new file mode 100644
index 000000000..df0fad60d
--- /dev/null
+++ b/public/scripts/extensions/translate/index.js
@@ -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 = `
+
+
+
+
+
+
+
+
+
`;
+
+ $('#extensions_settings').append(html);
+ for (const [key, value] of Object.entries(languageCodes)) {
+ $('#translation_target_language').append(`
`);
+ }
+
+ 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');
+});
diff --git a/public/scripts/extensions/translate/manifest.json b/public/scripts/extensions/translate/manifest.json
new file mode 100644
index 000000000..401db9627
--- /dev/null
+++ b/public/scripts/extensions/translate/manifest.json
@@ -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"
+}
diff --git a/public/scripts/extensions/translate/style.css b/public/scripts/extensions/translate/style.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/public/style.css b/public/style.css
index 599e7caa8..4d8a2e8db 100644
--- a/public/style.css
+++ b/public/style.css
@@ -233,6 +233,7 @@ table.responsiveTable {
text-align: center;
}
+.mes_translate,
.sd_message_gen,
.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.translate .mes_translate,
body.tts .mes_narrate {
display: inline-block;
}
diff --git a/server.js b/server.js
index fb48eb6d4..76a84513f 100644
--- a/server.js
+++ b/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) {
if (!fs.existsSync(SECRETS_FILE)) {
const emptyFile = JSON.stringify({});