Sleazy Fork is available in English.

Booru Cotrans

Tranlate images on boorus with cotrans.touhou.ai! Hit F8 to switch betweeen tranlated and original image. Hit F9 for configuration.

Verze ze dne 16. 11. 2023. Zobrazit nejnovější verzi.

// ==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';
        }
    };
}