// ==UserScript==
// @name Booru Cotrans
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Tranlate images on boorus with cotrans.touhou.ai! Hit F8 to switch betweeen tranlated and original image. Hit F9 for configuration.
// @author Rhodan81
// @match https://gelbooru.com/index.php?page=post*
// @match https://rule34.xxx/index.php?page=post*
// @match https://chan.sankakucomplex.com/de/posts/*
// @match https://booru.allthefallen.moe/posts/*
// @match https://*.booru.org/index.php?page=post*
// @connect gelbooru.com
// @connect img3.gelbooru.com
// @connect rule34.xxx
// @connect wimg.rule34.xxx
// @connect api.cotrans.touhou.ai
// @connect s.sankakucomplex.com
// @connect chan.sankakucomplex.com
// @connect booru.allthefallen.moe
// @connect *.booru.org
// @connect img.booru.org
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @grant GM.setValue
// @grant GM_setValue
// @grant GM.getValue
// @grant GM_getValue
// @grant GM.deleteValue
// @grant GM_deleteValue
// @grant GM.addValueChangeListener
// @grant GM_addValueChangeListener
// @grant GM.removeValueChangeListener
// @grant GM_removeValueChangeListener
// ==/UserScript==
let gmc = new GM_config(
{
'id': 'BooruCotransConfig', // The id used for this instance of GM_config
'title': 'Booru Cotrans Settings', // Panel Title
'fields': // Fields object
{
'target_language': // This is the id of the field
{
'label': 'Target language', // Appears next to field
'type': 'select', // Makes this setting a text field
'options': ['English', 'Chinese (Simplified)', 'Chinese (Traditional)', 'Czech', 'Dutch', 'French', 'German', 'Hungarian', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese (Brazil)', 'Romanian', 'Russian', 'Spanish', 'Türkish', 'Ukrainian', 'Vietnames', 'Arabic', 'Serbian', 'Croation', 'Thai'], // Possible choices
'default': 'English' // Default value if user doesn't change it
},
'translator':
{
'label': 'Translation engine',
'type': 'select',
'options': ['gpt3.5', 'google','youdao','baidu','deepl','papago','offline'],
'default': 'gpt3.5'
},
'size':
{
'label': 'Translation resolution',
'type': 'select',
'options': ['1024x1024', '1536x1536','2048x2048','2560x2560'],
'default': '1536x1536'
},
'direction':
{
'label': 'Text direction',
'type': 'select',
'options': ['Automatic', 'Horizontal','Vertical'],
'default': 'Automatic'
},
},
'events':
{
'save': function () { // runs after values are saved
if (tranlated_image_src != null) {
tranlated_image_src = null;
}
if (translated == true) {
untranlate_image();
tranlate_image();
}
this.close();
}
}
});
let GMP
{
// polyfill functions
const GMPFunctionMap = {
xmlHttpRequest: typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : undefined,
setValue: typeof GM_setValue !== 'undefined' ? GM_setValue : undefined,
getValue: typeof GM_getValue !== 'undefined' ? GM_getValue : undefined,
deleteValue: typeof GM_deleteValue !== 'undefined' ? GM_deleteValue : undefined,
addValueChangeListener: typeof GM_addValueChangeListener !== 'undefined' ? GM_addValueChangeListener : undefined,
removeValueChangeListener: typeof GM_removeValueChangeListener !== 'undefined' ? GM_removeValueChangeListener : undefined,
}
const xmlHttpRequest = GM.xmlHttpRequest.bind(GM) || GMPFunctionMap.xmlHttpRequest
GMP = new Proxy(GM, {
get(target, prop) {
if (prop === 'xmlHttpRequest') {
return (context) => {
return new Promise((resolve, reject) => {
xmlHttpRequest({
...context,
onload(event) {
context.onload?.()
resolve(event)
},
onerror(event) {
context.onerror?.()
reject(event)
},
})
})
}
}
if (prop in target) {
const v = target[prop]
return typeof v === 'function' ? v.bind(target) : v
}
if (prop in GMPFunctionMap && typeof GMPFunctionMap[prop] === 'function')
return GMPFunctionMap[prop]
console.error(`[Cotrans Manga Translator] GM.${prop} isn't supported in your userscript engine and it's required by this script. This may lead to unexpected behavior.`)
},
})
}
let image = document.getElementById('image');
let translated = false;
let tranlated_image_src;
let original_image_src;
const translating_idle_image = document.createElement("img");
translating_idle_image.src = 'data:image/png;base64,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';
translating_idle_image.style.position = 'fixed';
translating_idle_image.style.bottom = '50px';
translating_idle_image.style.left = '50px';
translating_idle_image.style.display = 'none';
document.body.appendChild(translating_idle_image);
function key_up(e) {
if (e.code === 'F8') {
if (translated == false)
tranlate_image();
else
untranlate_image();
}
else if (e.code === 'F9') {
gmc.open();
}
}
function get_language() {
switch(gmc.get('target_language')) {
case 'English':
return 'ENG';
case 'Chinese (Simplified)':
return 'CHS';
case 'Chinese (Traditional)':
return 'CHT';
case 'Czech':
return 'CSY';
case 'Dutch':
return 'NLD';
case 'French':
return 'FRA';
case 'German':
return 'DEU';
case 'Hungarian':
return 'HUN';
case 'Italian':
return 'ITA';
case 'Japanese':
return 'JPN';
case 'Korean':
return 'KOR';
case 'Polish':
return 'PLK';
case 'Portuguese (Brazil)':
return 'PTB';
case 'Romanian':
return 'ROM';
case 'Russian':
return 'RUS';
case 'Spanish':
return 'ESP';
case 'Türkish':
return 'TRK';
case 'Ukrainian':
return 'UKR';
case 'Vietnames':
return 'VIN';
case 'Arabic':
return 'ARA';
case 'Serbian':
return 'SRP';
case 'Croation':
return 'HRV';
case 'Thai':
return 'THA';
default:
return 'ENG';
}
}
function get_translator() {
return gmc.get('translator');
}
function get_size() {
switch(gmc.get('size')) {
case '1024x1024':
return 'S';
case '1536x1536':
return 'M';
case '2048x2048':
return 'L';
case '2560x2560':
return 'X';
default:
return 'M';
}
}
function get_direction() {
switch(gmc.get('direction')) {
case 'Automatic':
return 'auto';
case 'Horizontal':
return 'h';
case 'Vertical':
return 'v';
default:
return 'auto';
}
}
document.addEventListener('keyup', key_up, false);
async function pullTranslationStatusPolling(id) {
while (true) {
console.info('Polling translation result');
const res = await GMP.xmlHttpRequest({
method: "GET",
url: `https://api.cotrans.touhou.ai/task/${id}/status/v1`
});
const msg = JSON.parse(res.responseText);
if (msg.type === "result") {
return msg.result;
}
await new Promise(resolve => setTimeout(resolve, 1e3));
}
}
async function untranlate_image() {
translated = false;
image.src = original_image_src;
}
async function tranlate_image() {
if (tranlated_image_src != null) {
image.src = tranlated_image_src;
translated = true;
return;
}
console.info('Tranlating image');
translating_idle_image.style.display = 'unset';
const result_get_blob = await GMP.xmlHttpRequest({
method: "GET",
responseType: "blob",
url: image.src,
overrideMimeType: "text/plain; charset=x-user-defined"});
let file_blob = result_get_blob.response;
const form_data = new FormData();
form_data.append("file", file_blob);
form_data.append("target_language", get_language());
form_data.append("detector", "default");
form_data.append("direction", get_direction());
form_data.append("translator", get_translator());
form_data.append("size", get_size());
form_data.append("retry", "false");
const result = await GMP.xmlHttpRequest({
method: "POST",
url: "https://api.cotrans.touhou.ai/task/upload/v1",
// @ts-expect-error FormData is supported
data: form_data
});
let responseText = JSON.parse(result.responseText);
let mask_url = responseText.result?.translation_mask;
if (!mask_url) {
const res = await pullTranslationStatusPolling(responseText.id);
mask_url = res.translation_mask;
}
const c = document.createElement("canvas");
var ctx=c.getContext("2d");
var imageObj1 = new Image();
var imageObj2 = new Image();
imageObj1.src = URL.createObjectURL(file_blob);
imageObj1.onload = function() {
c.width = imageObj1.width;
c.height = imageObj1.height;
ctx.drawImage(imageObj1, 0, 0);
imageObj2.src = mask_url;
imageObj2.crossOrigin = "anonymous";
imageObj2.onload = function() {
ctx.drawImage(imageObj2, 0, 0);
original_image_src = image.src;
tranlated_image_src = c.toDataURL("image/png");
image.src = tranlated_image_src;
translated = true;
translating_idle_image.style.display = 'none';
}
};
}