// ==UserScript==
// @name NovelAI Mod
// @namespace https://www6.notion.site/dc99953d5f04405c893fba95dace0722
// @version 15
// @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 SUGGESTION_LIMIT = 1500;
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"]
};
// グローバル変数
let lastTyped = new Date();
let suggested_at = new Date();
let seedButton;
let chants = [];
let allTags = [];
let chantURL;// ユーティリティ関数
const gcd = (a, b) => b == 0 ? a : gcd(b, a % b);
const getAspect = whArray => whArray.map(x => x / gcd(whArray[0], whArray[1]));
const aspectList = (wa, ha, maxpixel = 1024 * 1024) => {
let aspect = wa / ha;
let limit = 16384;
let steps = 64;
let ws = Array.from({ length: (limit - steps) / steps + 1 }, (_, i) => (i + 1) * steps);
let hs = ws.slice();
return ws.flatMap(w => hs.map(h => w / h === aspect && w * h <= maxpixel ? [w, h] : null)).filter(Boolean);
};
const getChantURL = force => {
if (force === true) {
localStorage.removeItem("chantURL");
}
let url = 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 (url) localStorage.setItem("chantURL", url);
return url;
};
// 新しい関数: 現在表示されている画像のDataURLを取得
const getImageDataURL = async (mode) => {
try {
if (mode === "current") {
const imgs = Array.from(document.querySelectorAll('img[src]')).filter(x => x.offsetParent);
const url = imgs.reduce((max, img) => img.height > max.height ? img : max).src || document.querySelector('img[src]')?.src;
if (!url) return null;
const response = await fetch(url);
const blob = await response.blob();
return await new Promise(resolve => {
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
} else if (mode === "uploaded") {
const uploadInput = document.getElementById('naimodUploadedImage');
const previewImage = document.getElementById('naimodPreviewImage');
if (uploadInput && uploadInput.files[0]) {
return await new Promise(resolve => {
let reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(uploadInput.files[0]);
});
} else if (previewImage && previewImage.src) {
return previewImage.src;
}
return null;
}
} catch (error) {
console.error("Error getting image DataURL:", error);
return null;
}
};
// メイン機能関数
async function ImprovementTags(tags, imageDataURL) {
console.debug("ImprovementTags function called with tags:", tags);
// タグの_を半角スペースに置換
tags = tags.replace(/_/g, " ");
const apiKey = document.getElementById('geminiApiKey').value;
if (!apiKey) {
alert("Gemini API Keyが設定されていません。設定画面でAPIキーを入力してください。");
return false;
}
console.debug("API Key found:", apiKey.substring(0, 5) + "...");
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=${apiKey}`;
console.debug("ENDPOINT:", ENDPOINT);
let prompt = `以下に示すのはdanbooruのタグを用いて画像を生成するNovelAIの機能で、添付の画像を生成する為に用意したタグの一覧です。
${tags}
- 実際の画像の内容と列挙されたタグ一覧の、比較検討とブラッシュアップを行ってください。
- 不要と判断したタグを列挙してください。例えば顔しか映っていない画像なのにスカートや靴下に関する言及がある場合や、顔が映らない構図なのに瞳の色や表情に言及がある場合、黒髪のキャラしか居ないのにblonde hairと記述されるなど明らかに的外れなタグが含まれている場合などは、それらを除去する必要があります。(should remove)
- それとは逆に、新たに付与するべきであると考えられるタグがある場合は列挙してください。(should add)
- また、客観的に画像から読み取って付与するべきと判断したタグ以外に、追加することでより表現が豊かになると考えられるタグがあれば、それも提案してください。例えば文脈上付与した方がよりリアリティが増すと思われるアクセサリや小物を示すタグ、より効果的な表現を得る為に付与するのが好ましいと思われるエフェクトのタグなどです。
これらの内容は元々の画像には含まれていない要素であるべきです。(may add)
- 固有名詞と思われる認識不能なタグに関しては無視してください。
- また、NovelAI系ではない自然文のプロンプトで画像生成をする場合に用いるプロンプトも別途提案してください。実際の画像や上記のshouldRemove,shouldAdd,mayAddの内容を踏襲し、2000文字程度の自然言語の英文で表現してください。
返信はJSON形式で以下のスキーマに従ってください。
{
"shouldRemove": [string]
"shouldAdd": [string]
"mayAdd": [string],
"naturalLanguagePrompt": [string]
}
`;
console.debug("Prompt prepared:", prompt);
const payload = {
method: 'POST',
headers: {},
body: JSON.stringify({
contents: [
{
parts: [
{ text: prompt },
{ inline_data: { mime_type: "image/jpeg", data: imageDataURL.split(',')[1] } }
]
}
],
"generationConfig": {
"temperature": 1.0,
"max_output_tokens": 4096
},
safetySettings: [
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE"
}
]
}),
mode: 'cors'
};
console.debug("Payload prepared:", payload);
let result;
try {
console.debug("Sending fetch request to Gemini API");
const response = await fetch(ENDPOINT, payload);
console.debug("Response received:", response);
const data = await response.json();
console.debug("Response data:", data);
result = data.candidates[0].content.parts[0].text;
result = result.replace('```json\n', '').replace('\n```', '');
result = JSON.parse(result);
// 結果のタグも_を半角スペースに置換
result.shouldRemove = result.shouldRemove.map(tag => tag.replace(/_/g, " "));
result.shouldAdd = result.shouldAdd.map(tag => tag.replace(/_/g, " "));
result.mayAdd = result.mayAdd.map(tag => tag.replace(/_/g, " "));
console.debug("Parsed result:", result);
} catch (error) {
console.error('エラー:', error);
alert("AI改善の処理中にエラーが発生しました。");
return false;
}
return result || false;
}
const saveJpeg = async () => {
let seed = Array.from(seedButton.querySelectorAll("span")).find(x => x.textContent.match(/^[0-9]*$/))?.textContent;
let filename = `${seed}.jpg`;
try {
const dataURI = await getImageDataURL("current");
if (!dataURI) {
throw new Error("Failed to get image DataURL");
}
let image = new Image();
image.src = dataURI;
await new Promise(resolve => { image.onload = resolve; });
let canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").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();
} catch (error) {
console.error("Error saving JPEG:", error);
}
};
const changePixel = (w, h) => {
document.querySelectorAll('input[type="number"][step="64"]').forEach((x, i) => {
x.value = [w, h][i];
x._valueTracker = '';
x.dispatchEvent(new Event('input', { bubbles: true }));
});
};
const Crease = value => {
const getTagPower = tag => tag.split("").filter(x => x === "{").length - tag.split("").filter(x => x === "[").length;
let textarea = document.querySelector("textarea[placeholder]");
let selectionEnd = textarea.selectionEnd;
let oldTags = textarea.value.split(",").map(x => x.trim());
let targetTag = getTargetTag();
if (targetTag) {
let tags = oldTags.map(tag => {
if (tag === targetTag) {
let tagPower = getTagPower(targetTag) + value;
tag = tag.replace(/[\{\}\[\]]/g, "");
return tagPower > 0 ? '{'.repeat(tagPower) + tag + '}'.repeat(tagPower) :
tagPower < 0 ? '['.repeat(-tagPower) + tag + ']'.repeat(-tagPower) : tag;
}
return tag;
}).filter(Boolean);
textarea.value = tags.join(", ") + ", ";
textarea._valueTracker = '';
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
textarea.selectionEnd = selectionEnd;
}
};
const Append = (key, value) => {
let textarea = document.querySelector("textarea[placeholder]");
let selectionEnd = textarea.selectionEnd;
let oldTags = textarea.value.split(",").map(x => x.trim());
let targetTag = getTargetTag();
let newTags = key ? chants.find(x => x.name === key.trim())?.content.split(",").map(x => x.trim()) :
value ? [value.trim()] : [];
if (newTags?.length) {
let tags = oldTags.flatMap(tag =>
tag === targetTag ? (key ? [tag, ...newTags] : newTags) : [tag]
);
if (!targetTag) tags = [...tags, ...newTags];
tags = [...new Set(tags.filter(Boolean))];
textarea.value = tags.join(", ") + ", ";
textarea._valueTracker = '';
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
textarea.selectionEnd = selectionEnd;
}
};
const Suggest = () => {
console.debug("Suggest function called");
let textarea = document.querySelector("textarea[placeholder]");
if (textarea) {
let targetTag = getTargetTag()?.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
let suggestionField = document.getElementById("suggestionField");
suggestionField.textContent = "";
if (targetTag) {
let suggestions = allTags.filter(x =>
x.name.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0 ||
x.terms.some(y => y.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0)
).slice(0, 20);
let done = new Set();
suggestions.forEach(tag => {
if (!done.has(tag.name)) {
let button = document.createElement("button");
let count = tag.coumt > 1000 ? `${(Math.round(tag.coumt / 100) / 10)}k` : tag.coumt;
button.textContent = `${tag.name} (${count})`;
button.style.color = colors[tag.category][1];
button.title = tag.terms.filter(Boolean).join(", ");
button.onclick = () => Append(null, tag.name);
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 createTagButton = (tag, color, isEnabled) => {
let button = document.createElement('button');
button.textContent = tag;
button.className = "naimod-tag-button";
button.style.backgroundColor = color;
button.dataset.enabled = isEnabled ? 'true' : 'false';
const updateButtonStyle = () => {
if (button.dataset.enabled === 'true') {
button.style.opacity = '1';
button.style.textDecoration = 'none';
} else {
button.style.opacity = '0.5';
button.style.textDecoration = 'line-through';
}
};
updateButtonStyle();
button.onclick = () => {
button.dataset.enabled = button.dataset.enabled === 'true' ? 'false' : 'true';
updateButtonStyle();
};
return button;
};
const Build = async () => {
console.debug("Build function called");
// スタイルの定義を更新
if (!document.getElementById('naimod-styles')) {
const style = document.createElement('style');
style.id = 'naimod-styles';
style.textContent = `
#imageSource {
color: black;
}
.naimod-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
overflow: auto;
}
.naimod-settings-content {
padding: 1rem;
border: 1px solid rgb(204, 204, 204);
border-radius: 10px;
width: 90%;
max-width: 600px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 64, 0.9);
color: #fff;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.naimod-settings-title {
margin-bottom: 10px;
border-bottom: 2px solid #fff;
padding-bottom: 5px;
}
.naimod-input {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
.naimod-two-pane {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 50vh;
overflow-y: auto;
}
.naimod-pane-container {
display: flex;
flex-direction: column;
}
.naimod-pane {
border: 1px solid #ccc;
padding: 10px;
background-color: rgba(255, 255, 255, 0.1);
max-height: 20vh;
overflow-y: auto;
}
.naimod-button {
margin: 2px;
padding: 5px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.naimod-tag-button {
color: white;
margin: 2px;
padding: 5px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.naimod-accordion {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
cursor: pointer;
padding: 10px;
width: 100%;
text-align: left;
border: none;
outline: none;
transition: 0.4s;
display: flex;
justify-content: space-between;
align-items: center;
}
.naimod-panel {
padding: 0 10px;
background-color: rgba(255, 255, 255, 0.05);
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
.naimod-image-source-container {
margin-bottom: 10px;
}
.naimod-upload-container {
margin-top: 10px;
width: 200px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 5px;
display: none;
justify-content: center;
align-items: center;
cursor: pointer;
}
.naimod-preview-image {
max-width: 100%;
max-height: 100%;
display: none;
}
.naimod-natural-language-container {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: rgba(255, 255, 255, 0.1);
}
.naimod-natural-language-title {
margin-top: 0;
margin-bottom: 10px;
}
.naimod-natural-language-content {
white-space: pre-wrap;
margin: 0;
max-height: 20vh;
overflow-y: auto;
}
.naimod-close-button-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
@media (min-width: 768px) {
.naimod-two-pane {
flex-direction: row;
}
.naimod-pane-container {
flex: 1;
}
}
.accordion-arrow {
font-size: 12px;
transition: transform 0.3s ease;
}
.naimod-accordion.active .accordion-arrow {
transform: rotate(180deg);
}
`;
document.head.appendChild(style);
}
let modal = document.getElementById("naimod-modal");
if (!modal) {
modal = document.createElement("div");
modal.id = "naimod-modal";
modal.className = "naimod-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
document.body.appendChild(modal);
let settingsContent = document.createElement("div");
settingsContent.className = "naimod-settings-content";
let settingsTitle = document.createElement("h2");
settingsTitle.textContent = "Settings";
settingsTitle.className = "naimod-settings-title";
// アコーディオンボタンのスタイルを更新
let apiKeyAccordion = document.createElement("button");
apiKeyAccordion.className = "naimod-accordion";
apiKeyAccordion.innerHTML = "API Settings <span class='accordion-arrow'>▼</span>"; // 矢印を追加
let apiKeyPanel = document.createElement("div");
apiKeyPanel.className = "naimod-panel";
let apiKeyInput = document.createElement("input");
apiKeyInput.type = "text";
apiKeyInput.id = "geminiApiKey";
apiKeyInput.className = "naimod-input";
apiKeyInput.placeholder = "Enter Gemini API Key";
apiKeyInput.value = localStorage.getItem("geminiApiKey") || "";
apiKeyInput.addEventListener("change", () => {
localStorage.setItem("geminiApiKey", apiKeyInput.value);
});
let apiKeyLabel = document.createElement("label");
apiKeyLabel.htmlFor = "geminiApiKey";
apiKeyLabel.textContent = "Gemini API Key:";
apiKeyLabel.style.display = "block";
apiKeyLabel.style.marginBottom = "5px";
apiKeyPanel.appendChild(apiKeyLabel);
apiKeyPanel.appendChild(apiKeyInput);
let resetButton = createButton("Reset Chants URL", "red", () => {
chantURL = getChantURL(true);
Build();
modal.style.display = "none";
});
resetButton.className = "naimod-button";
resetButton.style.marginTop = "10px";
apiKeyPanel.appendChild(resetButton);
// アコーディオンの動作を更新
apiKeyAccordion.addEventListener("click", function () {
this.classList.toggle("active");
let panel = this.nextElementSibling;
let arrow = this.querySelector('.accordion-arrow');
if (panel.style.maxHeight) {
panel.style.maxHeight = null;
arrow.textContent = '▼'; // 閉じた状態
} else {
panel.style.maxHeight = panel.scrollHeight + "px";
arrow.textContent = '▲'; // 開いた状態
}
});
// 二段パネルの構造を変更
let twoPane = document.createElement("div");
twoPane.className = "naimod-two-pane";
let leftContainer = document.createElement("div");
leftContainer.className = "naimod-pane-container";
let rightContainer = document.createElement("div");
rightContainer.className = "naimod-pane-container";
let leftPane = document.createElement("div");
leftPane.className = "naimod-pane";
let rightPane = document.createElement("div");
rightPane.className = "naimod-pane right-pane";
let imageSourceContainer = document.createElement("div");
imageSourceContainer.className = "naimod-image-source-container";
let imageSourceLabel = document.createElement("label");
imageSourceLabel.textContent = "Image Source: ";
let imageSourceSelect = document.createElement("select");
imageSourceSelect.id = "imageSource";
["current", "uploaded"].forEach(source => {
let option = document.createElement("option");
option.value = source;
option.textContent = source.charAt(0).toUpperCase() + source.slice(1);
imageSourceSelect.appendChild(option);
});
let uploadContainer = document.createElement("div");
uploadContainer.className = "naimod-upload-container";
uploadContainer.textContent = "Drag & Drop or Click to Upload";
let uploadInput = document.createElement("input");
uploadInput.type = "file";
uploadInput.id = "naimodUploadedImage";
uploadInput.accept = "image/*";
uploadInput.style.display = "none";
// previewImage の作成を削除(handleImageUpload 内で必要に応じて作成されます)
const handleImageUpload = (file) => {
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const uploadContainer = document.querySelector('.naimod-upload-container');
if (uploadContainer) {
let previewImage = document.getElementById('naimodPreviewImage');
if (!previewImage) {
previewImage = document.createElement('img');
previewImage.id = 'naimodPreviewImage';
previewImage.className = 'naimod-preview-image';
}
previewImage.src = e.target.result;
previewImage.style.display = "block";
uploadContainer.textContent = "";
uploadContainer.appendChild(previewImage);
console.debug("Image uploaded successfully:", previewImage.src.substring(0, 50) + "...");
} else {
console.error("Upload container not found");
}
};
reader.onerror = (error) => {
console.error("Error reading file:", error);
alert("画像の読み込み中にエラーが発生しました。");
};
reader.readAsDataURL(file);
} else {
console.error("Invalid file type:", file ? file.type : "No file");
alert("有効な画像ファイルを選択してください。");
}
};
uploadInput.addEventListener("change", (e) => {
if (e.target.files && e.target.files[0]) {
handleImageUpload(e.target.files[0]);
} else {
console.error("No file selected");
alert("ファイルが選択されていません。");
}
});
uploadContainer.addEventListener("click", () => {
uploadInput.click();
});
uploadContainer.addEventListener("dragover", (e) => {
e.preventDefault();
uploadContainer.style.borderColor = "#000";
});
uploadContainer.addEventListener("dragleave", () => {
uploadContainer.style.borderColor = "#ccc";
});
uploadContainer.addEventListener("drop", (e) => {
e.preventDefault();
uploadContainer.style.borderColor = "#ccc";
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleImageUpload(e.dataTransfer.files[0]);
} else {
console.error("No file dropped");
alert("ファイルのドロップに失敗しました。");
}
});
// クリップボードからの画像貼り付け
document.addEventListener("paste", (e) => {
if (imageSourceSelect.value === "uploaded") {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const blob = items[i].getAsFile();
handleImageUpload(blob);
break;
}
}
}
});
imageSourceSelect.addEventListener("change", () => {
uploadContainer.style.display = imageSourceSelect.value === "uploaded" ? "flex" : "none";
});
// 初期状態を設定
imageSourceSelect.value = "current";
uploadContainer.style.display = "none";
imageSourceContainer.appendChild(imageSourceLabel);
imageSourceContainer.appendChild(imageSourceSelect);
imageSourceContainer.appendChild(uploadContainer);
// suggestButton の onclick イベントを修正
let suggestButton = createButton("Suggest AI Improvements", "blue", async () => {
let textarea = document.querySelector("textarea[placeholder]");
if (textarea) {
let tags = textarea.value.replace(/_/g, " "); // _を半角スペースに置換
rightPane.innerHTML = "<div>Loading AI suggestions...</div>";
suggestButton.disabled = true;
suggestButton.textContent = "Processing...";
let imageSource = document.getElementById("imageSource").value;
console.debug("Selected image source:", imageSource); // デバッグ用
let imageDataURL = await getImageDataURL(imageSource);
if (!imageDataURL) {
console.error("Failed to get image DataURL"); // デバッグ用
rightPane.innerHTML = "<div>Failed to get image. Please check your image source.</div>";
suggestButton.disabled = false;
suggestButton.textContent = "Suggest AI Improvements";
return;
}
console.debug("Image DataURL obtained:", imageDataURL.substring(0, 50) + "..."); // デバッグ用
let result = await ImprovementTags(tags, imageDataURL);
suggestButton.disabled = false;
suggestButton.textContent = "Suggest AI Improvements";
if (result) {
let originalTags = tags.split(',').map(t => t.trim()).filter(x => x);
rightPane.innerHTML = '';
// 元のタグを黒色のボタンで表示(shouldRemoveに含まれるものは赤色)
let originalTagsDiv = document.createElement('div');
originalTagsDiv.style.marginBottom = '10px';
originalTags.forEach(tag => {
let color = result.shouldRemove.includes(tag) ? 'red' : 'black';
let isEnabled = !result.shouldRemove.includes(tag);
originalTagsDiv.appendChild(createTagButton(tag, color, isEnabled));
});
rightPane.appendChild(originalTagsDiv);
// shouldAddを緑色のボタンで表示
if (result.shouldAdd.length > 0) {
let shouldAddDiv = document.createElement('div');
shouldAddDiv.style.marginBottom = '10px';
result.shouldAdd.forEach(tag => {
shouldAddDiv.appendChild(createTagButton(tag.replace(/_/g, " "), 'green', true));
});
rightPane.appendChild(shouldAddDiv);
}
// mayAddを青色のボタンで表示
if (result.mayAdd.length > 0) {
let mayAddDiv = document.createElement('div');
mayAddDiv.style.marginBottom = '10px';
result.mayAdd.forEach(tag => {
mayAddDiv.appendChild(createTagButton(tag.replace(/_/g, " "), 'blue', false));
});
rightPane.appendChild(mayAddDiv);
}
// naturalLanguagePrompt を更新
let naturalLanguagePromptElement = document.getElementById("naturalLanguagePrompt");
if (naturalLanguagePromptElement && result.naturalLanguagePrompt) {
naturalLanguagePromptElement.textContent = result.naturalLanguagePrompt;
} else {
naturalLanguagePromptElement.textContent = "No natural language prompt available.";
}
} else {
rightPane.innerHTML = "<div>Failed to get AI suggestions.</div>";
document.getElementById("naturalLanguagePrompt").textContent = "Failed to get natural language prompt.";
}
updateLeftPane();
}
});
suggestButton.style.marginBottom = "10px";
let applyButton = createButton("Apply Suggestions", "green", () => {
let textarea = document.querySelector("textarea[placeholder]");
if (textarea) {
let enabledTags = Array.from(rightPane.querySelectorAll('button[data-enabled="true"]'))
.map(button => button.textContent.replace(/_/g, " "));
textarea.value = enabledTags.join(", ");
textarea._valueTracker = '';
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
});
applyButton.style.marginBottom = "10px";
leftContainer.appendChild(suggestButton);
leftContainer.appendChild(leftPane);
rightContainer.appendChild(applyButton);
rightContainer.appendChild(rightPane);
twoPane.appendChild(leftContainer);
twoPane.appendChild(rightContainer);
// naturalLanguagePrompt を表示するための新しい要素を追加
let naturalLanguageContainer = document.createElement("div");
naturalLanguageContainer.className = "naimod-natural-language-container";
let naturalLanguageTitle = document.createElement("h3");
naturalLanguageTitle.className = "naimod-natural-language-title";
naturalLanguageTitle.textContent = "Natural Language Prompt";
let naturalLanguageContent = document.createElement("p");
naturalLanguageContent.id = "naturalLanguagePrompt";
naturalLanguageContent.className = "naimod-natural-language-content";
naturalLanguageContent.textContent = "-";
naturalLanguageContainer.appendChild(naturalLanguageTitle);
naturalLanguageContainer.appendChild(naturalLanguageContent);
let closeButtonContainer = document.createElement("div");
closeButtonContainer.className = "naimod-close-button-container";
let closeButton = createButton("Close", "grey", () => modal.style.display = "none");
closeButton.style.padding = "5px 15px";
closeButtonContainer.appendChild(closeButton);
settingsContent.appendChild(settingsTitle);
settingsContent.appendChild(apiKeyAccordion);
settingsContent.appendChild(apiKeyPanel);
settingsContent.appendChild(imageSourceContainer);
settingsContent.appendChild(twoPane);
settingsContent.appendChild(naturalLanguageContainer); // 新しく追加
settingsContent.appendChild(closeButtonContainer);
// テキストエリアの内容を左ペインに表示する関数
function updateLeftPane() {
let textarea = document.querySelector("textarea[placeholder]");
if (textarea) {
let tags = textarea.value.split(",").map(x => x.trim().replace(/_/g, " ")).filter(x => x);
leftPane.innerHTML = '';
tags.forEach(tag => {
let button = document.createElement('button');
button.textContent = tag;
button.style.backgroundColor = 'black';
button.style.color = 'white';
button.style.margin = '2px';
button.style.padding = '5px';
button.style.border = 'none';
button.style.borderRadius = '3px';
button.style.cursor = 'default'; // カーソルをデフォルトに設定
button.disabled = true; // ボタンを無効化
leftPane.appendChild(button);
});
}
}
// テキストエリアの内容が変更されたときに左ペインを更新
document.addEventListener("input", function (e) {
if (e.target.tagName.toLowerCase() === "textarea") {
updateLeftPane();
}
});
// モーダルが表示されたときに左ペインを更新
modal.addEventListener("show", updateLeftPane);
// 初期表示時に左ペインを更新
updateLeftPane();
modal.appendChild(settingsContent);
// モーダルの外側をクリックしたときに閉じる
modal.onclick = (event) => {
if (event.target === modal) {
modal.style.display = "none";
}
};
}
let ui = document.getElementById("NAIModUi") || (() => {
let el = document.createElement("div");
el.id = "NAIModUi";
el.style.padding = "0.5rem";
return el;
})();
ui.innerHTML = '';
let hr = document.createElement("hr");
hr.style.color = "grey";
hr.style.borderWidth = "2px";
let [optionField, chantsField, suggestionField] = ['option', 'chants', 'suggestion'].map(id => {
let el = document.createElement("div");
el.id = `${id}Field`;
return el;
});
[optionField, hr.cloneNode(), chantsField, hr.cloneNode(), suggestionField].forEach(el => ui.appendChild(el));
let openModalButton = createButton("Open Settings", "blue", () => {
modal.style.display = "block";
});
optionField.appendChild(openModalButton);
let clearButton = createButton("Clear Suggestion", "red", () => {
document.getElementById("suggestionField").textContent = "";
});
optionField.appendChild(clearButton);
let jpegButton = document.getElementById("jpegButton") || createButton("JPEG", "", saveJpeg);
jpegButton.id = "jpegButton";
let jpegQuality = document.getElementById("jpegQuality") || document.createElement("div");
jpegQuality.id = "jpegQuality";
let input = document.getElementById("jpegQualityInput") || document.createElement("input");
Object.assign(input, {
value: 0.85,
id: "jpegQualityInput",
type: "number",
step: "0.01",
max: "1",
min: "0",
style: "maxWidth: 3rem"
});
if (!document.getElementById("jpegQualityInput")) {
jpegQuality.appendChild(input);
}
// seedButtonの更新(必要に応じて)
seedButton = Array.from(document.querySelectorAll('span[style]'))
.find(x => x.textContent.trim().match(/^[0-9]*(seed|シード)/i))?.closest("button");
if (seedButton) {
[jpegButton, jpegQuality].forEach(el => {
seedButton.classList.forEach(x => el.classList.add(x));
if (!document.getElementById(el.id)) {
seedButton.parentNode.insertBefore(el, seedButton);
}
});
}
try {
const response = await fetch(chantURL);
chants = await response.json();
chants.forEach(chant => {
let button = createButton(chant.name, colors[chant.color][1], () => Append(chant.name, null));
chantsField.appendChild(button);
});
putUI(ui);
} catch (error) {
console.error("Error fetching chants:", error);
}
};
// ヘルパー関数
const getTargetTag = () => {
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];
return oldTags[oldTags.indexOf(targetTag) + 1]?.trim();
}
return null;
};
const createButton = (text, color, onClick) => {
let button = document.createElement("button");
button.textContent = text;
button.style.color = color;
button.addEventListener("click", onClick);
return button;
};
const putUI = ui => {
document.querySelector("textarea[placeholder]")?.parentElement.appendChild(ui);
seedButton = Array.from(document.querySelectorAll('span[style]'))
.find(x => x.textContent.trim().match(/^[0-9]*(seed|シード)/i))?.closest("button");
if (seedButton && !document.getElementById("jpegButton")) {
let jpegButton = createButton("JPEG", "", saveJpeg);
jpegButton.id = "jpegButton";
let jpegQuality = document.createElement("div");
let input = document.createElement("input");
Object.assign(input, {
value: 0.85,
id: "jpegQuality",
type: "number",
step: "0.01",
max: "1",
min: "0",
style: "maxWidth: 3rem"
});
jpegQuality.appendChild(input);
[jpegButton, jpegQuality].forEach(el => {
seedButton.classList.forEach(x => el.classList.add(x));
seedButton.parentNode.insertBefore(el, seedButton);
});
}
let widthInput = document.querySelector('input[type="number"][step="64"]');
if (widthInput) {
let displayAspect = document.querySelector("#displayAspect") || createDisplayAspect();
widthInput.parentNode.appendChild(displayAspect);
updateAspectInputs();
}
};
const createDisplayAspect = () => {
let displayAspect = document.createElement("div");
displayAspect.id = "displayAspect";
let container = document.createElement("div");
container.style.maxWidth = "130px";
container.style.display = "flex";
let [widthAspect, heightAspect] = ['width', 'height'].map(type => {
let input = document.createElement("input");
input.type = "number";
return input;
});
let largeCheck = document.createElement("input");
largeCheck.type = "checkbox";
largeCheck.title = "Check to large size.";
let submitButton = document.createElement("input");
submitButton.type = "submit";
submitButton.value = "Aspect => Pixel";
[widthAspect, heightAspect].forEach(input => {
document.querySelector('input[type="number"][step="64"]').classList.forEach(x => input.classList.add(x));
});
[widthAspect, heightAspect, largeCheck].forEach(el => container.appendChild(el));
[container, submitButton].forEach(el => displayAspect.appendChild(el));
submitButton.addEventListener("click", () => {
let wa = widthAspect.value;
let ha = heightAspect.value;
let maxpixel = largeCheck.checked ? 1664 * 1664 : 1024 * 1024;
let as = aspectList(wa, ha, maxpixel);
if (as.length) {
changePixel(...as[as.length - 1]);
}
});
return displayAspect;
};
const updateAspectInputs = () => {
let displayAspect = document.querySelector("#displayAspect");
if (displayAspect) {
let [widthAspect, heightAspect] = displayAspect.querySelectorAll("input");
let Aspect = getAspect(Array.from(document.querySelectorAll('input[type="number"][step="64"]')).map(x => x.value));
[widthAspect.value, heightAspect.value] = Aspect;
}
};// イベントリスナー
document.addEventListener("blur", event => {
if (event.target.tagName.toLowerCase() === 'button') {
Build();
}
}, true);
document.addEventListener("keydown", e => {
if (e.ctrlKey && e.code === "ArrowUp") {
e.preventDefault();
Crease(1);
}
if (e.ctrlKey && e.code === "ArrowDown") {
e.preventDefault();
Crease(-1);
}
});
document.addEventListener("keyup", e => {
if (e.target.tagName === "TEXTAREA") {
const now = new Date();
lastTyped = now;
if (now - suggested_at > SUGGESTION_LIMIT) {
Suggest();
suggested_at = now;
} else {
setTimeout(() => {
if (lastTyped === now) { // アイドル状態の確認
Suggest();
suggested_at = new Date();
}
}, 1000);
}
}
});
document.addEventListener("click", e => {
if (e.target.tagName === "TEXTAREA") {
const now = new Date();
if (now - suggested_at > SUGGESTION_LIMIT / 2) {
Suggest();
suggested_at = now;
} else {
setTimeout(() => {
if (lastTyped === now) { // アイドル状態の確認
Suggest();
suggested_at = new Date();
}
}, 1000);
}
}
});
// 初期化
let init = setInterval(() => {
if (document.querySelector("textarea[placeholder]")?.offsetParent) {
clearInterval(init);
chantURL = getChantURL(null);
Build();
}
});
// タグデータの取得
fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru.json")
.then(response => response.json())
.then(data => {
allTags = data;
return fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru_wiki.slim.json");
})
.then(response => response.json())
.then(wikiPages => {
allTags = allTags.map(x => {
let wikiPage = wikiPages.find(y => y.name === x.name);
if (wikiPage) {
x.terms = [...new Set([...x.terms, ...wikiPage.otherNames])];
}
return x;
});
});
})();