// ==UserScript==
// @name NovelAI Mod
// @namespace https://www6.notion.site/dc99953d5f04405c893fba95dace0722
// @version 14
// @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 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 putUI = function (ui) {
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));
widthAspect.value = Aspect[0];
heightAspect.value = Aspect[1];
}
}
}
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 () {
let ui = document.getElementById("NAIModUi");
if (!ui) {
ui = document.createElement("div");
ui.setAttribute("id", "NAIModUi");
ui.style.padding = "0.5rem";
}
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);
});
putUI(ui);
});
}
document.addEventListener("blur", function (event) {
if (event.target.tagName.toLowerCase() === 'button') {
Build();
}
}, true);
let init = setInterval(() => {
document.querySelectorAll("textarea[placeholder]").forEach(textarea => {
if (textarea.offsetParent) {
clearInterval(init);
Build();
}
});
});
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();
}
});
})();