Sleazy Fork is available in English.

NovelAI Mod

Extention for NovelAI Image Generator. Fast Suggestion, Chant, Save in JPEG, Get aspect ratio.

// ==UserScript==
// @name         NovelAI Mod
// @namespace    https://www6.notion.site/dc99953d5f04405c893fba95dace0722
// @version      13
// @description  Extention for NovelAI Image Generator. Fast Suggestion, Chant, Save in JPEG, Get aspect ratio.
// @author       SenY
// @match        https://novelai.net/image
// @icon         https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    const gcd = function(a, b){
        if(b == 0) return a;
        return gcd(b, a % b);
    }
    const getAspect = function(whArray){
        return whArray.map(x => x / gcd(whArray[0], whArray[1]));
    }
    const aspectList = function(wa, ha, maxpixel) {
        if( !maxpixel ){
            maxpixel = 1024 * 1024;
        }
        let aspect = wa / ha;
        let limit = 16384;
        let steps = 64;
        let ws = []
        for(let i=steps;i<=limit;i+=steps){
            ws.push(i);
        }
        let hs = ws.slice(0, ws.length+1);
        let results = [];
        ws.forEach(w => {
            hs.forEach(h => {
                if(w / h == aspect && w * h <= maxpixel){
                    results.push([w, h]);
                }
            })
        });
        return results;
    }

    let suggested_at = new Date();
    const colors = {
        "-1": ["red", "maroon"],
        "0": ["lightblue", "dodgerblue"],
        "1": ["gold", "goldenrod"],
        "3": ["violet", "darkorchid"],
        "4": ["lightgreen", "darkgreen"],
        "5": ["tomato", "darksalmon"],
        "6": ["red", "maroon"],
        "7": ["whitesmoke", "black"],
        "8": ["seagreen", "darkseagreen"]
    }
    const getChantURL = function (force) {
        if (force === true) {
            localStorage.removeItem("chantURL");
        }
        let chantURL = localStorage.getItem("chantURL") || prompt("Input your chants json url.\nThe URL must be a Cors-enabled server (e.g., gist.github.com).", "https://gist.githubusercontent.com/vkff5833/989808aadebf8648831955cdf2a7b3e3/raw/yuuri.json");
        if (chantURL) {
            localStorage.setItem("chantURL", chantURL);
        }
        return chantURL;
    }
    let chantURL = getChantURL(null);
    let ui = document.createElement("div");
    ui.style.padding = "0.5rem";


    let seedButton;
    const saveJpeg = function () {
        let seed = Array.from(seedButton.querySelectorAll("span")).map(x => x.textContent).find(x => x.search(/^[0-9]*$/) == 0);
        let filename = seed + ".jpg";
        let imgs = Array.from(document.querySelectorAll('img[src]')).filter(x => x.offsetParent);
        let url = imgs.find(x => x.height == Math.max.apply(null, imgs.map(x => x.height))).getAttribute("src");
        if ( !url ){
            url = document.querySelector('img[src]').getAttribute("src");
        }
        let blob = fetch(url).then(r => r.blob()).then(blob => {
            let fileReader = new FileReader();
            fileReader.onload = function () {
                let dataURI = this.result;
                let canvas = document.createElement("canvas");
                let context = canvas.getContext("2d");
                let image = new Image();
                image.src = dataURI;
                image.onload = function () {
                    canvas.width = image.width;
                    canvas.height = image.height;
                    context.drawImage(image, 0, 0);
                    let JPEG = canvas.toDataURL("image/jpeg", document.getElementById("jpegQuality").value - 0);
                    let link = document.createElement('a');
                    link.href = JPEG;
                    link.download = filename;
                    link.click();
                };
            }
            fileReader.readAsDataURL(blob);
        });
    }
    let jpegButton = document.createElement("div");
    let jpegQuality = document.createElement("div");
    jpegButton.textContent = "JPEG";
    jpegButton.addEventListener("click", () => {
        saveJpeg();
    });
    jpegQuality.textContent = "";
    let input = document.createElement("input");
    input.value = 0.85;
    input.setAttribute("id", "jpegQuality");
    input.setAttribute("type", "number");
    input.setAttribute("step", "0.01");
    input.setAttribute("max", "1");
    input.setAttribute("min", "0");
    input.style.maxWidth = "3rem";
    jpegQuality.appendChild(input);

    const changePixel = function(w, h) {
        let wh = [w, h];
        let i = 0;
        document.querySelectorAll('input[type="number"][step="64"]').forEach(x => {
            x.value = wh[i];
            x._valueTracker = '';
            x.dispatchEvent(new Event('input', { bubbles: true }));
            i++;
        });
    }

    const createUI = function(){
        document.querySelectorAll("textarea[placeholder]").forEach(textarea => {
            if( textarea.offsetParent ){
                textarea.parentElement.appendChild(ui);
            }
        });
        seedButton = Array.from(document.querySelectorAll('span[style]')).find(x => x.textContent.trim().search(/^[0-9]*(seed|シード)/i) == 0);
        if(seedButton){
            seedButton = seedButton.closest("button");
            seedButton.classList.forEach(x => {
                jpegButton.classList.add(x);
                jpegQuality.classList.add(x);
            });
            seedButton.parentNode.insertBefore(jpegButton, seedButton);
            seedButton.parentNode.insertBefore(jpegQuality, seedButton);
        }
        let widthInput = document.querySelector('input[type="number"][step="64"]');
        if ( widthInput ){
            let heightInput = document.querySelectorAll('input[type="number"][step="64"]')[1];
            let displayAspect = document.querySelector("#displayAspect");
            if( !displayAspect ){
                displayAspect = document.createElement("div");
                displayAspect.setAttribute("id", "displayAspect");
                let container = document.createElement("div");
                container.style.maxWidth = "130px";
                container.style.display = "flex";
                let widthAspect = document.createElement("input");
                let heightAspect = document.createElement("input");
                let largeCheck = document.createElement("input");
                let submitButton = document.createElement("input");
                widthAspect.setAttribute("type", "number");
                heightAspect.setAttribute("type", "number");
                largeCheck.setAttribute("type", "checkbox");
                largeCheck.setAttribute("title", "Check to large size.");
                submitButton.setAttribute("type", "submit");
                submitButton.value = "Aspect => Pixel";
                widthInput.classList.forEach(x => {
                    widthAspect.classList.add(x);
                    heightAspect.classList.add(x);
                });
                container.appendChild( widthAspect );
                container.appendChild( heightAspect );
                container.appendChild( largeCheck );
                displayAspect.appendChild(container);
                displayAspect.appendChild(submitButton);
                widthInput.parentNode.appendChild( displayAspect );
                const changePixelByAspect = function(){
                    let wa = widthAspect.value;
                    let ha = heightAspect.value;
                    let maxpixel = 1024 * 1024;
                    if( largeCheck.checked ){
                        maxpixel = 1664 * 1664;
                    }
                    let as = aspectList(wa, ha, maxpixel);
                    if(as.length){
                        let wh = as[as.length-1];
                        changePixel(wh[0], wh[1]);
                    }
                }
                submitButton.addEventListener("click", function(){
                    changePixelByAspect();
                });
            }else{
                let widthAspect = displayAspect.querySelectorAll("input")[0];
                let heightAspect = displayAspect.querySelectorAll("input")[1];
                let Aspect = getAspect(Array.from(document.querySelectorAll('input[type="number"][step="64"]')).map(x => x.value));
                console.log(widthAspect, heightAspect);
                widthAspect.value = Aspect[0];
                heightAspect.value = Aspect[1];
            }
        }
    }


    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.type === 'childList') {
                if (mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach((node) => {
                        if (node.tagName == "SPAN"){
                            createUI();
                        }
                    })
                }
            }
        });
    });

    // 監視を開始する
    observer.observe(document.body, {
        childList: true, // 直接の子ノードの変更を監視
        subtree: true, // すべての子孫ノードの変更も監視
    });

    let allTags = [];
    fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru.json", {
        method: "GET",
    }).then((response) => response.json())
        .then((data) => {
            allTags = data;
            fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru_wiki.slim.json", {
                method: "GET",
            }).then((response) => response.json())
                .then((wikiPages) => {
                    allTags = allTags.map(x => {
                        let wikiPage = wikiPages.find(y => y.name == x.name);
                        if (wikiPage) {
                            x.terms = x.terms.concat(wikiPage.otherNames);
                        }
                        return x;
                    });
                });
        });
    let chants = [];

    const getTargetTag = function () {
        let textarea = document.querySelector("textarea[placeholder]");
        if (textarea) {
            let oldTags = textarea.value.split(",").map(x => x.trim());
            let beforeTags = textarea.value.slice(0, textarea.selectionEnd).split(",").map(x => x.trim());
            let targetTag = beforeTags[beforeTags.length - 2];
            targetTag = oldTags[oldTags.indexOf(targetTag) + 1];
            return targetTag.trim();
        } else {
            return null;
        }
    };
    const Crease = function (value) {
        const getTagPower = function (tag) {
            let tagPower = 0;
            tagPower += tag.split("").filter(x => x == "{").length;
            tagPower -= tag.split("").filter(x => x == "[").length;
            return tagPower;
        }
        let textarea = document.querySelector("textarea[placeholder]");
        let selectionEnd = textarea.selectionEnd;
        let oldTags = textarea.value.split(",").map(x => x.trim());
        let targetTag = getTargetTag();
        let tags = [];
        if (targetTag) {
            oldTags.forEach(tag => {
                if (tag == targetTag) {
                    let tagPower = getTagPower(targetTag) + value;
                    tag = tag.replace(/[\{\}\[\]]/g, "", tag);
                    for (let i = 0; i < Math.abs(tagPower); i++) {
                        if (tagPower > 0) {
                            tag = '{' + tag + '}';
                        }
                        if (tagPower < 0) {
                            tag = '[' + tag + ']';
                        }
                    }
                }
                tags.push(tag);
            });
            tags = tags.filter(x => x);
            textarea.textContent = tags.join(", ").replace(/^, /g, "");
            textarea.value = tags.join(", ").replace(/^, /g, "") + ", ";
            textarea._valueTracker = '';
            textarea.dispatchEvent(new Event('input', { bubbles: true }));
            textarea.focus();
            textarea.selectionEnd = selectionEnd;
        }
    }

    const Append = function (key, value) {
        let targetTag = getTargetTag();
        let text;
        let newTags;
        let textarea = document.querySelector("textarea[placeholder]");
        let selectionEnd = textarea.selectionEnd;
        let oldTags = textarea.value.split(",").map(x => x.trim());
        if (key) {
            let chant = chants.find(x => x.name == key.trim());
            if (chant) {
                newTags = chant.content.split(",").map(x => x.trim());
            }
        }

        if (value) {
            newTags = [value.trim()];
        }
        if (newTags.length) {
            //let tags = oldTags.concat(newTags).filter(x => x);
            let tags = [];
            oldTags.forEach(tag => {
                if (tag == targetTag) {
                    if (key) {
                        tags.push(tag);
                    }
                    newTags.forEach(newTag => {
                        tags.push(newTag);
                    });
                } else {
                    tags.push(tag);
                }
            });
            if (!targetTag) {
                tags = tags.concat(newTags);
            }
            tags = Array.from(new Set(tags.filter(x => x)));
            textarea.textContent = tags.join(", ").replace(/^, /g, "");
            textarea.value = tags.join(", ").replace(/^, /g, "") + ", ";
            // Special Thanks to https://fate.5ch.net/test/read.cgi/liveuranus/1702763225/730
            textarea._valueTracker = '';
            textarea.dispatchEvent(new Event('input', { bubbles: true }));
            textarea.focus();
            textarea.selectionEnd = selectionEnd;
        }
    }
    const Suggest = function () {
        let textarea = document.querySelector("textarea[placeholder]");
        if (textarea) {
            let tags = textarea.value.split(",").map(x => x.trim());
            if (tags.length) {
                let targetTag = getTargetTag().replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
                document.getElementById("suggestionField").textContent = "";
                let suggestions = [];
                if (targetTag) {
                    suggestions = allTags.filter(x => x.name.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0).slice(0, 10);
                    suggestions = suggestions.concat(allTags.filter(x => {
                        return x.terms.map(y => y.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0).includes(true);
                    }).slice(0, 20));
                }
                let done = new Set();
                suggestions.forEach(tag => {
                    if (!done.has(tag.name)) {
                        let button = document.createElement("button");
                        let count = tag.coumt;
                        if (count > 1000) {
                            count /= 1000;
                            count = Math.round(count * 10) / 10;
                            count += "k";
                        }
                        button.textContent = tag.name + " (" + count + ")";
                        button.style.color = colors[tag.category][1];
                        button.setAttribute("title", tag.terms.map(x => x.trim()).filter(x => x).join(", "));
                        button.addEventListener("click", function () {
                            Append(null, tag.name);
                        });
                        document.getElementById("suggestionField").appendChild(button);
                        if (textarea.value.split(",").map(y => y.trim().replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")).includes(tag.name.replace(/_/g, " "))) {
                            button.style.opacity = 0.5;
                        }
                    }
                    done.add(tag.name);
                });
            }
        }
    }

    const Build = function () {
        ui.textContent = "";
        let hr = document.createElement("hr");
        hr.style.color = "grey";
        hr.style.borderWidth = "2px";
        let optionField = document.createElement("div");
        let chantsField = document.createElement("div");
        let suggestionField = document.createElement("div");
        suggestionField.setAttribute("id", "suggestionField");
        ui.appendChild(optionField);
        ui.appendChild(hr.cloneNode());
        ui.appendChild(chantsField);
        ui.appendChild(hr.cloneNode());
        ui.appendChild(suggestionField);

        let resetButton = document.createElement("button");
        resetButton.textContent = "Reset Chants URL";
        resetButton.style.color = "red";
        resetButton.addEventListener("click", function () {
            chantURL = getChantURL(true);
            Build();
        });
        optionField.appendChild(resetButton);
        let clearButton = document.createElement("button");
        clearButton.textContent = "Clear Suggestion";
        clearButton.style.color = "red";
        clearButton.addEventListener("click", function () {
            document.getElementById("suggestionField").textContent = "";
        });
        optionField.appendChild(clearButton);


        fetch(chantURL, {
            method: "GET",
        }).then((response) => response.json())
            .then((data) => {
                chants = data;
                chants.forEach(chant => {
                    let button = document.createElement("button");
                    button.textContent = chant.name;
                    button.style.color = colors[chant.color][1];
                    button.addEventListener("click", function () {
                        Append(chant.name, null);
                    });
                    chantsField.appendChild(button);
                });
            });
    }
    let init = false;
    document.addEventListener("DOMContentLoaded", function () {
        if (init === false) {
            Build();
            init = true;
        }
    });
    document.addEventListener("keydown", function (e) {
        if (e.ctrlKey == true && e.code == "ArrowUp") {
            e.preventDefault();
            Crease(1);
        }
        if (e.ctrlKey == true && e.code == "ArrowDown") {
            e.preventDefault();
            Crease(-1);
        }
    });
    const SUGGESTION_LIMIT = 300;
    document.addEventListener("keyup", function (e) {
        let typed = new Date();
        if ( typed - suggested_at > SUGGESTION_LIMIT && e.target.tagName == "TEXTAREA") {
            Suggest();
            suggested_at = typed;
        }
    });
    document.addEventListener("click", function (e) {
        if (e.target.tagName == "TEXTAREA") {
            Suggest();
        }
    });
})();