togacat's picture
Upload 2 files
93dc2f1 verified
Raw
History Blame Contribute Delete
49 kB
<!doctype html>
<html lang="ja">
<!--
# Copyright 2024 togacat/werecatpete. All rights reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-->
<head>
<meta charset="utf-8" />
<title>Mitsua Likes Explorer 3x3 + Inpaint</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 0;
background: #222;
color: #eee;
}
header {
padding: 12px 16px;
background: #333;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
}
header > * {
margin-right: 8px;
}
textarea {
width: 320px;
height: 60px;
resize: vertical;
}
input[type="number"], select {
width: 90px;
}
button {
padding: 6px 12px;
border-radius: 4px;
border: none;
background: #4a8af4;
color: white;
cursor: pointer;
margin-top: 4px;
display: inline-block;
}
button:disabled {
background: #555;
cursor: default;
}
main {
padding: 8px;
}
#status {
padding: 4px 8px;
font-size: 0.9rem;
color: #ccc;
}
#normalMode {
max-width: 900px;
margin: 0 auto;
}
#gridContainer {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
margin-top: 4px;
}
.cell {
position: relative;
background: #111;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.cell img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
cursor: pointer;
display: block;
}
.cell.center {
outline: 3px solid #f4d24a;
outline-offset: -2px;
}
#credit {
margin-top: 4px;
text-align: right;
font-size: 0.8rem;
color: #aaa;
}
#dropHint {
position: fixed;
right: 16px;
bottom: 16px;
font-size: 0.8rem;
background: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #555;
}
.field-group {
margin-bottom: 4px;
}
/* Inpaint mode */
#inpaintMode {
display: none;
max-width: 1000px;
margin: 0 auto;
}
#inpaintTools {
margin-bottom: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
#inpaintTools label {
font-size: 0.85rem;
}
#inpaintCanvasWrapper {
position: relative;
display: inline-block;
background: #111;
}
#inpaintImage {
display: block;
max-width: 100%;
height: auto;
}
#maskCanvas {
position: absolute;
left: 0;
top: 0;
pointer-events: auto;
}
.color-dot {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 4px;
vertical-align: middle;
border: 1px solid #fff;
}
</style>
</head>
<body>
<header>
<div>
<label>Prompt<br>
<textarea id="prompt"></textarea>
</label>
</div>
<div>
<label>Negative Prompt<br>
<textarea id="negativePrompt"></textarea>
</label>
</div>
<div>
<div class="field-group">
<label>Width<br>
<input id="width" type="number" value="672" min="64" step="8">
</label>
</div>
<div class="field-group">
<label>Height<br>
<input id="height" type="number" value="896" min="64" step="8">
</label>
</div>
<div class="field-group">
<label>Seed<br>
<input id="seed" type="number" value="1000" step="1">
</label>
</div>
</div>
<div>
<div class="field-group">
<label>Steps<br>
<input id="steps" type="number" value="40" min="1" step="1">
</label>
</div>
<div class="field-group">
<label>Guidance Scale<br>
<input id="guidanceScale" type="number" value="6.0" step="0.1">
</label>
</div>
<div class="field-group">
<label>Guidance Rescale<br>
<input id="guidanceRescale" type="number" value="0.7" step="0.05">
</label>
</div>
</div>
<div>
<button id="generateOneBtn">Generate (1枚ずつ)</button><br>
<button id="generateGridBtn">Generate (3x3 一括)</button><br>
<button id="generateAutoOneBtn">Auto (1枚ずつ)</button><br>
<button id="generateAutoGridBtn">Auto (3x3 一括)</button><br>
<button id="stopAutoBtn" disabled>Stop</button><br>
<button id="searchBtn" disabled>Search</button><br>
<button id="inpaintBtn" disabled>Inpaint</button><br>
<label style="display:inline-flex;align-items:center;margin-top:4px;font-size:0.85rem;">
<input id="noLogo" type="checkbox" style="margin-right:4px;">
<span>No logo(生成画像を共有する場合、作品にMitsua Likesのクレジット表示は必須)</span>
</label>
<label style="display:inline-flex;align-items:center;margin-top:4px;font-size:0.85rem;">
<input id="disableCharDetectNormal" type="checkbox" style="margin-right:4px;">
<span>キャラ検知モード無効化(無効の場合はパブリックキャラの再生成に注意)</span>
</label>
</div>
</header>
<div id="status">準備完了</div>
<main>
<!-- 通常モード -->
<div id="normalMode">
<div id="gridContainer"></div>
</div>
<!-- Inpaint モード -->
<div id="inpaintMode">
<div id="inpaintTools">
<button id="inpaintNormalBtn">Normal</button>
<button id="repaintBtn">Repaint</button>
<button id="resetInpaintBtn">Reset</button>
<label> Brush:
<input id="brushSize" type="number" value="20" min="1" max="200" step="1">
</label>
<button id="penBtn">Pen</button>
<button id="eraserBtn">Eraser</button>
<button id="clearMaskBtn">Clear Mask</button>
<label> Color:
<select id="maskColorSelect">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<span class="color-dot" id="colorDot" style="background:#ff0000;"></span>
</label>
<label> i2i %:
<input id="i2iStrength" type="number" value="100" min="0" max="100" step="5">
</label>
<button id="i2iBtn">i2i (全体)</button>
<label style="display:inline-flex;align-items:center;font-size:0.85rem;">
<input id="disableCharDetectInpaint" type="checkbox" style="margin-right:4px;">
<span>キャラ検知モード無効化(無効の場合はパブリックキャラの再生成に注意)</span>
</label>
</div>
<div id="inpaintCanvasWrapper">
<img id="inpaintImage" src="" alt="inpaint base">
<canvas id="maskCanvas"></canvas>
</div>
<div style="margin-top:4px;font-size:0.8rem;color:#ccc;">
Inpaint モード: 画像をドラッグ&ドロップして再編集可能(PNG と .pt をまとめてドロップできます)
</div>
</div>
<div id="credit">Generated by Mitsua Likes</div>
</main>
<div id="dropHint">通常: PNG をドラッグ&ドロップで中央に読み込み / Inpaint中: PNG+PTをドロップ</div>
<script>
const promptEl = document.getElementById("prompt");
const negativePromptEl = document.getElementById("negativePrompt");
const widthEl = document.getElementById("width");
const heightEl = document.getElementById("height");
const seedEl = document.getElementById("seed");
const stepsEl = document.getElementById("steps");
const guidanceScaleEl = document.getElementById("guidanceScale");
const guidanceRescaleEl = document.getElementById("guidanceRescale");
const noLogoEl = document.getElementById("noLogo");
const disableCharDetectNormalEl = document.getElementById("disableCharDetectNormal");
const disableCharDetectInpaintEl = document.getElementById("disableCharDetectInpaint");
const generateOneBtn = document.getElementById("generateOneBtn");
const generateGridBtn = document.getElementById("generateGridBtn");
const generateAutoOneBtn = document.getElementById("generateAutoOneBtn");
const generateAutoGridBtn = document.getElementById("generateAutoGridBtn");
const stopAutoBtn = document.getElementById("stopAutoBtn");
const searchBtn = document.getElementById("searchBtn");
const inpaintBtn = document.getElementById("inpaintBtn");
const statusEl = document.getElementById("status");
const normalModeEl = document.getElementById("normalMode");
const inpaintModeEl = document.getElementById("inpaintMode");
const gridContainer = document.getElementById("gridContainer");
const inpaintNormalBtn = document.getElementById("inpaintNormalBtn");
const repaintBtn = document.getElementById("repaintBtn");
const resetInpaintBtn = document.getElementById("resetInpaintBtn");
const brushSizeEl = document.getElementById("brushSize");
const penBtn = document.getElementById("penBtn");
const eraserBtn = document.getElementById("eraserBtn");
const clearMaskBtn = document.getElementById("clearMaskBtn");
const maskColorSelect = document.getElementById("maskColorSelect");
const colorDot = document.getElementById("colorDot");
const i2iStrengthEl = document.getElementById("i2iStrength");
const i2iBtn = document.getElementById("i2iBtn");
const inpaintImageEl = document.getElementById("inpaintImage");
const maskCanvas = document.getElementById("maskCanvas");
const maskCtx = maskCanvas.getContext("2d");
const inpaintCanvasWrapper = document.getElementById("inpaintCanvasWrapper");
const GRID_SIZE = 9;
const CENTER_INDEX = 4;
let gridImages = new Array(GRID_SIZE).fill(null);
let currentCenter = null;
let isManualGenerating = false;
let isAutoGenerating = false;
let autoIndex = 0;
let stopRequested = false;
let inpaintMode = false;
let currentInpaint = null; // { imageInfo, latentFilename }
let inpaintInitialState = null; // 最初に inpaint に入ったときの状態
// inpaint 描画状態
let drawing = false;
let drawMode = "pen"; // "pen" or "eraser"
let maskColor = "rgba(255,0,0,0.4)";
// キャラ検知モード無効化(通常/インペイントで状態を同期)
if (disableCharDetectNormalEl && disableCharDetectInpaintEl) {
// 初期同期
disableCharDetectInpaintEl.checked = disableCharDetectNormalEl.checked;
disableCharDetectNormalEl.addEventListener("change", () => {
disableCharDetectInpaintEl.checked = disableCharDetectNormalEl.checked;
});
disableCharDetectInpaintEl.addEventListener("change", () => {
disableCharDetectNormalEl.checked = disableCharDetectInpaintEl.checked;
});
}
function setStatus(msg) {
statusEl.textContent = msg;
console.log(msg);
}
function updateButtonStates() {
const busy = isManualGenerating || isAutoGenerating;
generateOneBtn.disabled = busy || inpaintMode;
generateGridBtn.disabled = busy || inpaintMode;
generateAutoOneBtn.disabled = busy || inpaintMode;
generateAutoGridBtn.disabled = busy || inpaintMode;
stopAutoBtn.disabled = !busy;
searchBtn.disabled = !currentCenter || busy || inpaintMode;
inpaintBtn.disabled = !currentCenter || busy || inpaintMode;
repaintBtn.disabled = busy || !inpaintMode || !currentInpaint;
resetInpaintBtn.disabled = !inpaintMode || !inpaintInitialState;
}
function renderGrid() {
gridContainer.innerHTML = "";
for (let i = 0; i < GRID_SIZE; i++) {
const cell = document.createElement("div");
cell.className = "cell";
if (i === CENTER_INDEX) {
cell.classList.add("center");
}
const info = gridImages[i];
if (info) {
const img = document.createElement("img");
img.src = info.url;
img.title = `seed=${info.seed}`;
img.addEventListener("click", () => onCellClick(i));
cell.appendChild(img);
}
gridContainer.appendChild(cell);
}
}
function fillInputsFromMeta(meta) {
if (!meta) return;
if (meta.prompt !== undefined) promptEl.value = meta.prompt;
if (meta.negative_prompt !== undefined) negativePromptEl.value = meta.negative_prompt;
if (meta.width !== undefined) widthEl.value = meta.width;
if (meta.height !== undefined) heightEl.value = meta.height;
if (meta.steps !== undefined) stepsEl.value = meta.steps;
if (meta.guidance_scale !== undefined) guidanceScaleEl.value = meta.guidance_scale;
if (meta.guidance_rescale !== undefined) guidanceRescaleEl.value = meta.guidance_rescale;
if (meta.seed !== undefined) seedEl.value = meta.seed;
}
function onCellClick(index) {
const info = gridImages[index];
if (!info) return;
if (index !== CENTER_INDEX) {
const tmp = gridImages[CENTER_INDEX];
gridImages[CENTER_INDEX] = gridImages[index];
gridImages[index] = tmp;
}
currentCenter = gridImages[CENTER_INDEX];
fillInputsFromMeta(currentCenter);
renderGrid();
setStatus(`選択: seed=${currentCenter.seed}`);
updateButtonStates();
}
async function generateOneImage(overrides = {}) {
const basePrompt = promptEl.value.trim();
const baseNegativePrompt = negativePromptEl.value.trim();
const baseWidth = parseInt(widthEl.value, 10) || 672;
const baseHeight = parseInt(heightEl.value, 10) || 896;
const baseSteps = parseInt(stepsEl.value, 10) || 40;
const baseGuidanceScale = parseFloat(guidanceScaleEl.value) || 6.0;
const baseGuidanceRescale = parseFloat(guidanceRescaleEl.value) || 0.7;
const payload = {
prompt: overrides.prompt !== undefined ? overrides.prompt : basePrompt,
negative_prompt: overrides.negative_prompt !== undefined ? overrides.negative_prompt : baseNegativePrompt,
width: overrides.width !== undefined ? overrides.width : baseWidth,
height: overrides.height !== undefined ? overrides.height : baseHeight,
steps: overrides.steps !== undefined ? overrides.steps : baseSteps,
guidance_scale: overrides.guidance_scale !== undefined ? overrides.guidance_scale : baseGuidanceScale,
guidance_rescale: overrides.guidance_rescale !== undefined ? overrides.guidance_rescale : baseGuidanceRescale,
no_logo: noLogoEl && noLogoEl.checked,
disable_character_detection: disableCharDetectNormalEl && disableCharDetectNormalEl.checked
};
if (overrides.seed !== undefined && overrides.seed !== null && !Number.isNaN(overrides.seed)) {
payload.seed = overrides.seed;
}
const res = await fetch("/api/generate-one", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload)
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
return data;
}
async function handleGenerateOne() {
if (inpaintMode) return;
stopRequested = false;
isManualGenerating = true;
isAutoGenerating = false;
updateButtonStates();
gridImages = new Array(GRID_SIZE).fill(null);
currentCenter = null;
renderGrid();
setStatus("Generate (1枚ずつ): 3x3 画像を順に生成中...");
const basePrompt = promptEl.value.trim();
const baseNegativePrompt = negativePromptEl.value.trim();
const baseWidth = parseInt(widthEl.value, 10) || 672;
const baseHeight = parseInt(heightEl.value, 10) || 896;
const baseSteps = parseInt(stepsEl.value, 10) || 40;
const baseGuidanceScale = parseFloat(guidanceScaleEl.value) || 6.0;
const baseGuidanceRescale = parseFloat(guidanceRescaleEl.value) || 0.7;
const firstSeedVal = parseInt(seedEl.value, 10);
const useFirstSeed = !Number.isNaN(firstSeedVal);
try {
for (let i = 0; i < GRID_SIZE; i++) {
if (stopRequested) break;
setStatus(`Generate (1枚ずつ): ${i + 1}/${GRID_SIZE} 枚目生成中...`);
let overrides = {
prompt: basePrompt,
negative_prompt: baseNegativePrompt,
width: baseWidth,
height: baseHeight,
steps: baseSteps,
guidance_scale: baseGuidanceScale,
guidance_rescale: baseGuidanceRescale
};
if (i === 0 && useFirstSeed) {
overrides.seed = firstSeedVal;
}
const imgInfo = await generateOneImage(overrides);
gridImages[i] = imgInfo;
if (i === CENTER_INDEX && !stopRequested) {
currentCenter = imgInfo;
fillInputsFromMeta(currentCenter);
}
renderGrid();
}
if (stopRequested) {
setStatus("Generate (1枚ずつ) を停止しました");
} else {
setStatus("Generate (1枚ずつ) 完了");
}
} catch (err) {
console.error(err);
setStatus("Generate (1枚ずつ) 中にエラーが発生しました: " + err.message);
} finally {
isManualGenerating = false;
updateButtonStates();
stopRequested = false;
}
}
async function handleGenerateGrid() {
if (inpaintMode) return;
stopRequested = false;
isManualGenerating = true;
isAutoGenerating = false;
updateButtonStates();
gridImages = new Array(GRID_SIZE).fill(null);
currentCenter = null;
renderGrid();
setStatus("Generate (3x3 一括): 画像を生成中...");
const basePrompt = promptEl.value.trim();
const baseNegativePrompt = negativePromptEl.value.trim();
const baseWidth = parseInt(widthEl.value, 10) || 672;
const baseHeight = parseInt(heightEl.value, 10) || 896;
const baseSteps = parseInt(stepsEl.value, 10) || 40;
const baseGuidanceScale = parseFloat(guidanceScaleEl.value) || 6.0;
const baseGuidanceRescale = parseFloat(guidanceRescaleEl.value) || 0.7;
const firstSeedVal = parseInt(seedEl.value, 10);
const useFirstSeed = !Number.isNaN(firstSeedVal);
const payload = {
prompt: basePrompt,
negative_prompt: baseNegativePrompt,
width: baseWidth,
height: baseHeight,
steps: baseSteps,
guidance_scale: baseGuidanceScale,
guidance_rescale: baseGuidanceRescale,
first_seed: useFirstSeed ? firstSeedVal : null,
no_logo: noLogoEl && noLogoEl.checked,
disable_character_detection: disableCharDetectNormalEl && disableCharDetectNormalEl.checked
};
try {
const res = await fetch("/api/generate-grid9", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload)
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
const images = data.images || [];
for (let i = 0; i < GRID_SIZE && i < images.length; i++) {
gridImages[i] = images[i];
if (i === CENTER_INDEX) {
currentCenter = images[i];
fillInputsFromMeta(currentCenter);
}
renderGrid();
}
setStatus("Generate (3x3 一括) 完了");
} catch (err) {
console.error(err);
setStatus("Generate (3x3 一括) 中にエラーが発生しました: " + err.message);
} finally {
isManualGenerating = false;
updateButtonStates();
stopRequested = false;
}
}
async function startAutoGenerateOne() {
if (inpaintMode) return;
if (isAutoGenerating) return;
stopRequested = false;
isAutoGenerating = true;
isManualGenerating = false;
updateButtonStates();
setStatus("Auto (1枚ずつ) 開始(Stop で停止)");
const firstSeedVal = parseInt(seedEl.value, 10);
const useFirstSeed = !Number.isNaN(firstSeedVal);
let usedFirstSeedTopLeft = false;
try {
while (isAutoGenerating && !stopRequested) {
const idx = autoIndex;
autoIndex = (autoIndex + 1) % GRID_SIZE;
let overrides = {};
if (idx === 0 && useFirstSeed && !usedFirstSeedTopLeft) {
overrides.seed = firstSeedVal;
usedFirstSeedTopLeft = true;
}
setStatus(`Auto (1枚ずつ): index ${idx} に生成中...`);
const imgInfo = await generateOneImage(overrides);
gridImages[idx] = imgInfo;
if (!currentCenter && gridImages[CENTER_INDEX]) {
currentCenter = gridImages[CENTER_INDEX];
fillInputsFromMeta(currentCenter);
}
renderGrid();
updateButtonStates();
}
} catch (err) {
console.error(err);
setStatus("Auto (1枚ずつ) 中にエラーが発生しました: " + err.message);
} finally {
isAutoGenerating = false;
updateButtonStates();
if (stopRequested) {
setStatus("Auto (1枚ずつ) を停止しました");
} else {
setStatus("Auto (1枚ずつ) 停止");
}
stopRequested = false;
}
}
async function startAutoGenerateGrid() {
if (inpaintMode) return;
if (isAutoGenerating) return;
stopRequested = false;
isAutoGenerating = true;
isManualGenerating = false;
updateButtonStates();
setStatus("Auto (3x3 一括) 開始(Stop で停止)");
const basePrompt = promptEl.value.trim();
const baseNegativePrompt = negativePromptEl.value.trim();
const baseWidth = parseInt(widthEl.value, 10) || 672;
const baseHeight = parseInt(heightEl.value, 10) || 896;
const baseSteps = parseInt(stepsEl.value, 10) || 40;
const baseGuidanceScale = parseFloat(guidanceScaleEl.value) || 6.0;
const baseGuidanceRescale = parseFloat(guidanceRescaleEl.value) || 0.7;
const firstSeedVal = parseInt(seedEl.value, 10);
const useFirstSeed = !Number.isNaN(firstSeedVal);
let usedFirstSeedTopLeft = false;
try {
while (isAutoGenerating && !stopRequested) {
const payload = {
prompt: basePrompt,
negative_prompt: baseNegativePrompt,
width: baseWidth,
height: baseHeight,
steps: baseSteps,
guidance_scale: baseGuidanceScale,
guidance_rescale: baseGuidanceRescale,
first_seed: (!usedFirstSeedTopLeft && useFirstSeed) ? firstSeedVal : null,
no_logo: noLogoEl && noLogoEl.checked,
disable_character_detection: disableCharDetectNormalEl && disableCharDetectNormalEl.checked
};
setStatus("Auto (3x3 一括): 生成中...");
const res = await fetch("/api/generate-grid9", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload)
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
const images = data.images || [];
for (let i = 0; i < GRID_SIZE && i < images.length; i++) {
gridImages[i] = images[i];
}
if (!currentCenter && gridImages[CENTER_INDEX]) {
currentCenter = gridImages[CENTER_INDEX];
fillInputsFromMeta(currentCenter);
}
renderGrid();
updateButtonStates();
usedFirstSeedTopLeft = true;
}
} catch (err) {
console.error(err);
setStatus("Auto (3x3 一括) 中にエラーが発生しました: " + err.message);
} finally {
isAutoGenerating = false;
updateButtonStates();
if (stopRequested) {
setStatus("Auto (3x3 一括) を停止しました");
} else {
setStatus("Auto (3x3 一括) 停止");
}
stopRequested = false;
}
}
function stopAll() {
if (!isAutoGenerating && !isManualGenerating) return;
stopRequested = true;
// ★ここを消す(生成中フラグは落とさない)
// isAutoGenerating = false;
// isManualGenerating = false;
updateButtonStates();
setStatus("停止要求を受け付けました(現在の画像生成が終わったら停止します)");
}
function randRange(min, max) {
return min + Math.random() * (max - min);
}
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function handleSearch() {
if (!currentCenter) {
setStatus("Search: まず中央画像を選択してください");
return;
}
if (isManualGenerating || isAutoGenerating || inpaintMode) return;
const basePrompt = promptEl.value.trim();
const baseNegativePrompt = negativePromptEl.value.trim();
const baseWidth = parseInt(widthEl.value, 10) || 672;
const baseHeight = parseInt(heightEl.value, 10) || 896;
const baseSteps = parseInt(stepsEl.value, 10) || 40;
const baseGuidanceScale = parseFloat(guidanceScaleEl.value) || 6.0;
const baseGuidanceRescale = parseFloat(guidanceRescaleEl.value) || 0.7;
const seedVal = parseInt(seedEl.value, 10);
const useSeed = !Number.isNaN(seedVal);
stopRequested = false;
setStatus("Search: 周囲8枚を生成中...");
isManualGenerating = true;
updateButtonStates();
try {
// 右下用のランダムワードを1つだけ取得
const rwRes = await fetch("/api/random-word");
let randomWord = "extra";
if (rwRes.ok) {
const rwJson = await rwRes.json();
if (rwJson.word) randomWord = rwJson.word;
}
// steps ±
let stMinus = baseSteps + randInt(-15, -1);
let stPlus = baseSteps + randInt(1, 15);
stMinus = Math.max(1, stMinus);
stPlus = Math.max(1, stPlus);
// guidance_scale ±
const gsMinus = baseGuidanceScale + randRange(-2.0, -0.1);
const gsPlus = baseGuidanceScale + randRange(0.1, 2.0);
// カンマ区切りのトークン配列
const tokens = basePrompt
.split(",")
.map(t => t.trim())
.filter(t => t.length > 0);
// 1回だけシャッフルしたプロンプトを作る(トークンの追加削除はしない)
function shuffledPromptOnce() {
if (tokens.length <= 1) return basePrompt;
const arr = tokens.slice();
for (let i = arr.length - 1; i > 0; i--) {
const j = randInt(0, i);
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
return arr.join(",");
}
// 1トークンだけ抜いたプロンプト
function promptRemovedOnce() {
if (tokens.length <= 1) return basePrompt;
const idx = randInt(0, tokens.length - 1);
const removed = tokens.slice(0, idx).concat(tokens.slice(idx + 1));
return removed.join(",");
}
// 左上・右上: シャッフル(順番のみ変更)
let promptShuffleLeft = basePrompt;
let promptShuffleRight = basePrompt;
if (tokens.length > 1) {
// 左上用シャッフル
promptShuffleLeft = shuffledPromptOnce();
// 右上用は、左上とは違う並びになるよう、左上の配列を回転させて作る
const leftTokens = promptShuffleLeft.split(",").map(t => t.trim()).filter(t => t.length > 0);
if (leftTokens.length > 1) {
const rotated = leftTokens.slice(1).concat(leftTokens[0]);
promptShuffleRight = rotated.join(",");
} else {
// トークン数1個しかない場合などは、そのまま
promptShuffleRight = promptShuffleLeft;
}
}
// 左下: 1トークンだけ抜いたもの
const promptRemovedBottom = promptRemovedOnce();
// 右下: ランダムワードを1つ足したもの
const promptWord = basePrompt ? (randomWord +"," + basePrompt) : randomWord;
const neighborConfigs = [
// 上下: steps ±
{ index: 1, prompt: basePrompt, steps: stMinus, guidance_scale: baseGuidanceScale }, // 上: steps -
{ index: 7, prompt: basePrompt, steps: stPlus, guidance_scale: baseGuidanceScale }, // 下: steps +
// 左右: guidance scale ±
{ index: 3, prompt: basePrompt, steps: baseSteps, guidance_scale: gsMinus }, // 左: gs -
{ index: 5, prompt: basePrompt, steps: baseSteps, guidance_scale: gsPlus }, // 右: gs +
// 左上・右上: プロンプト順番のランダムシャッフル(追加・削除なし)
{ index: 0, prompt: promptShuffleLeft, steps: baseSteps, guidance_scale: baseGuidanceScale }, // 左上: shuffle1
{ index: 2, prompt: promptShuffleRight, steps: baseSteps, guidance_scale: baseGuidanceScale }, // 右上: shuffle2
// 左下: 1トークン削除
{ index: 6, prompt: promptRemovedBottom, steps: baseSteps, guidance_scale: baseGuidanceScale }, // 左下: token-
// 右下: ランダムワードを1つ追加
{ index: 8, prompt: promptWord, steps: baseSteps, guidance_scale: baseGuidanceScale } // 右下: word+
];
for (const cfg of neighborConfigs) {
if (stopRequested) break;
setStatus(`Search: index ${cfg.index} を生成中...`);
const overrides = {
prompt: cfg.prompt,
negative_prompt: baseNegativePrompt,
width: baseWidth,
height: baseHeight,
steps: cfg.steps,
guidance_scale: cfg.guidance_scale,
guidance_rescale: baseGuidanceRescale
};
if (useSeed) {
overrides.seed = seedVal;
}
const imgInfo = await generateOneImage(overrides);
gridImages[cfg.index] = imgInfo;
renderGrid();
}
if (stopRequested) {
setStatus("Search を停止しました");
} else {
setStatus("Search 完了(周囲8枚を1枚ずつ更新)");
}
} catch (err) {
console.error(err);
setStatus("Search 中にエラーが発生しました: " + err.message);
} finally {
isManualGenerating = false;
updateButtonStates();
stopRequested = false;
}
}
// ---- Inpaint モード関連 ----
function enterInpaintMode() {
if (!currentCenter) {
setStatus("Inpaint: まず中央画像を選択してください");
return;
}
inpaintMode = true;
normalModeEl.style.display = "none";
inpaintModeEl.style.display = "block";
isManualGenerating = false;
isAutoGenerating = false;
stopRequested = false;
// 最初に inpaint モードに入ったときの状態を保存
inpaintInitialState = {
imageInfo: { ...currentCenter },
latentFilename: currentCenter.latent_filename || null,
prompt: promptEl.value.trim(),
negativePrompt: negativePromptEl.value.trim(),
width: parseInt(widthEl.value, 10) || currentCenter.width,
height: parseInt(heightEl.value, 10) || currentCenter.height,
steps: parseInt(stepsEl.value, 10) || currentCenter.steps,
guidance_scale: parseFloat(guidanceScaleEl.value) || currentCenter.guidance_scale,
guidance_rescale: parseFloat(guidanceRescaleEl.value) || currentCenter.guidance_rescale,
seed: parseInt(seedEl.value, 10) || currentCenter.seed,
};
// 画像を表示
currentInpaint = {
imageInfo: currentCenter,
latentFilename: currentCenter.latent_filename || null
};
inpaintImageEl.src = currentCenter.url;
inpaintImageEl.onload = () => {
resizeMaskCanvasToImage();
clearMask();
};
// latent が無い場合もある (古い画像) → サーバ側で latent を再計算
fetch("/api/inpaint-init", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ filename: currentCenter.filename })
}).then(r => r.json())
.then(j => {
if (j.latent_filename) {
currentInpaint.latentFilename = j.latent_filename;
if (inpaintInitialState && !inpaintInitialState.latentFilename) {
inpaintInitialState.latentFilename = j.latent_filename;
}
setStatus("Inpaint モード: latent ファイル検出 or 再計算完了");
} else if (j.error) {
setStatus("Inpaint init エラー: " + j.error);
} else {
setStatus("Inpaint モード: latent ファイルを用意できませんでした");
}
}).catch(e => {
console.error(e);
setStatus("Inpaint init エラー: " + e.message);
});
updateButtonStates();
}
function leaveInpaintMode() {
inpaintMode = false;
normalModeEl.style.display = "block";
inpaintModeEl.style.display = "none";
clearMask();
// 中央の画像を最新の inpaint 結果に更新
if (currentInpaint && currentInpaint.imageInfo) {
gridImages[CENTER_INDEX] = currentInpaint.imageInfo;
currentCenter = currentInpaint.imageInfo;
renderGrid();
fillInputsFromMeta(currentCenter);
}
inpaintInitialState = null;
updateButtonStates();
setStatus("Normal モードに戻りました");
}
function resizeMaskCanvasToImage() {
const width = inpaintImageEl.naturalWidth || inpaintImageEl.width;
const height = inpaintImageEl.naturalHeight || inpaintImageEl.height;
maskCanvas.width = width;
maskCanvas.height = height;
maskCanvas.style.width = width + "px";
maskCanvas.style.height = height + "px";
maskCtx.clearRect(0, 0, width, height);
}
function clearMask() {
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
}
function resetInpaintToInitial() {
if (!inpaintMode || !inpaintInitialState) {
setStatus("Reset: Inpaint 初期状態がありません");
return;
}
// 元画像+元 latent に完全に戻す
currentInpaint = {
imageInfo: { ...inpaintInitialState.imageInfo },
latentFilename: inpaintInitialState.latentFilename || null,
};
// 入力欄も最初の値に戻す
promptEl.value = inpaintInitialState.prompt || "";
negativePromptEl.value = inpaintInitialState.negativePrompt || "";
widthEl.value = inpaintInitialState.width || "";
heightEl.value = inpaintInitialState.height || "";
stepsEl.value = inpaintInitialState.steps || "";
guidanceScaleEl.value = inpaintInitialState.guidance_scale || "";
guidanceRescaleEl.value = inpaintInitialState.guidance_rescale || "";
seedEl.value = inpaintInitialState.seed || "";
// 画像だけを初期状態に戻す(マスクはそのまま残す)
inpaintImageEl.src = inpaintInitialState.imageInfo.url;
inpaintImageEl.onload = () => {
resizeMaskCanvasToImage();
// マスクは消さない
};
setStatus("Inpaint: 最初の画像にリセットしました");
updateButtonStates();
}
function setMaskColor(colorName) {
if (colorName === "red") {
maskColor = "rgba(255,0,0,0.4)";
colorDot.style.background = "#ff0000";
} else if (colorName === "green") {
maskColor = "rgba(0,255,0,0.4)";
colorDot.style.background = "#00ff00";
} else {
maskColor = "rgba(0,0,255,0.4)";
colorDot.style.background = "#0000ff";
}
}
function getCanvasPos(evt) {
const rect = maskCanvas.getBoundingClientRect();
const x = evt.clientX - rect.left;
const y = evt.clientY - rect.top;
return { x, y };
}
function startDrawing(evt) {
if (!inpaintMode) return;
drawing = true;
drawOnMask(evt);
}
function stopDrawing() {
drawing = false;
}
function drawOnMask(evt) {
if (!drawing) return;
const { x, y } = getCanvasPos(evt);
const r = parseInt(brushSizeEl.value, 10) || 20;
if (drawMode === "pen") {
maskCtx.fillStyle = maskColor;
maskCtx.beginPath();
maskCtx.arc(x, y, r, 0, Math.PI * 2);
maskCtx.fill();
} else {
// eraser
maskCtx.clearRect(x - r, y - r, r * 2, r * 2);
}
}
async function handleRepaint() {
if (!inpaintMode || !currentInpaint) {
setStatus("Repaint: Inpaint モードと latent が必要です");
return;
}
if (!currentInpaint.latentFilename) {
setStatus("Repaint: latent (.pt) が見つかりません(latents 付きで生成された画像か、PNG+PT をドロップしてください)");
return;
}
const baseInfo = currentInpaint.imageInfo;
const maskDataUrl = maskCanvas.toDataURL("image/png");
const payload = {
filename: baseInfo.filename,
latent_filename: currentInpaint.latentFilename,
prompt: promptEl.value.trim(),
negative_prompt: negativePromptEl.value.trim(),
width: parseInt(widthEl.value, 10) || baseInfo.width,
height: parseInt(heightEl.value, 10) || baseInfo.height,
steps: parseInt(stepsEl.value, 10) || baseInfo.steps,
guidance_scale: parseFloat(guidanceScaleEl.value) || baseInfo.guidance_scale,
guidance_rescale: parseFloat(guidanceRescaleEl.value) || baseInfo.guidance_rescale,
seed: parseInt(seedEl.value, 10) || baseInfo.seed,
strength: 0.5,
mask_png: maskDataUrl,
no_logo: noLogoEl && noLogoEl.checked,
disable_character_detection: disableCharDetectInpaintEl && disableCharDetectInpaintEl.checked
};
setStatus("Repaint: inpaint 実行中...");
try {
const res = await fetch("/api/inpaint-repaint", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload)
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
// 結果画像を inpaint ビューに反映
currentInpaint.imageInfo = data;
currentInpaint.latentFilename = data.latent_filename || currentInpaint.latentFilename;
inpaintImageEl.src = data.url;
inpaintImageEl.onload = () => {
resizeMaskCanvasToImage();
clearMask();
};
fillInputsFromMeta(data);
setStatus("Repaint 完了");
} catch (err) {
console.error(err);
setStatus("Repaint 中にエラーが発生しました: " + err.message);
}
}
async function handleI2I() {
if (!inpaintMode || !currentInpaint) {
setStatus("i2i: Inpaint モードと latent が必要です");
return;
}
if (!currentInpaint.latentFilename) {
setStatus("i2i: latent (.pt) が見つかりません(latents 付きで生成された画像か、PNG+PT をドロップしてください)");
return;
}
const baseInfo = currentInpaint.imageInfo;
// i2i 強度(%)を読み取り&0〜100にクランプ
let pct = parseFloat(i2iStrengthEl.value);
if (Number.isNaN(pct)) pct = 100;
pct = Math.max(0, Math.min(100, pct));
// mask_png は API の仕様上送るが、i2i モードではサーバ側で無視して全体マスクにする
const maskDataUrl = maskCanvas.toDataURL("image/png");
const payload = {
filename: baseInfo.filename,
latent_filename: currentInpaint.latentFilename,
prompt: promptEl.value.trim(),
negative_prompt: negativePromptEl.value.trim(),
width: parseInt(widthEl.value, 10) || baseInfo.width,
height: parseInt(heightEl.value, 10) || baseInfo.height,
steps: parseInt(stepsEl.value, 10) || baseInfo.steps,
guidance_scale: parseFloat(guidanceScaleEl.value) || baseInfo.guidance_scale,
guidance_rescale: parseFloat(guidanceRescaleEl.value) || baseInfo.guidance_rescale,
seed: parseInt(seedEl.value, 10) || baseInfo.seed,
strength: 0.5,
mask_png: maskDataUrl,
i2i_mode: true,
i2i_mask_pct: pct,
no_logo: noLogoEl && noLogoEl.checked,
disable_character_detection: disableCharDetectInpaintEl && disableCharDetectInpaintEl.checked
};
setStatus(`i2i: 画像全体を ${pct}% で再描画中...`);
try {
const res = await fetch("/api/inpaint-repaint", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload)
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const data = await res.json();
if (data.error) {
throw new Error(data.error);
}
// 結果を inpaint ビューに反映(Repaint と同じ処理)
currentInpaint.imageInfo = data;
currentInpaint.latentFilename = data.latent_filename || currentInpaint.latentFilename;
inpaintImageEl.src = data.url;
inpaintImageEl.onload = () => {
resizeMaskCanvasToImage();
clearMask();
};
fillInputsFromMeta(data);
setStatus("i2i 完了");
} catch (err) {
console.error(err);
setStatus("i2i 中にエラーが発生しました: " + err.message);
}
}
// ---- Drag & Drop ----
document.addEventListener("dragover", evt => {
evt.preventDefault();
evt.dataTransfer.dropEffect = "copy";
});
document.addEventListener("drop", async evt => {
evt.preventDefault();
const files = evt.dataTransfer.files;
if (!files || !files.length) return;
if (isManualGenerating || isAutoGenerating) {
setStatus("生成中はドロップできません");
return;
}
try {
if (!inpaintMode) {
// 通常モード: PNG を1つだけ扱う
const file = files[0];
if (!file.type.startsWith("image/")) {
setStatus("PNG 画像をドロップしてください");
return;
}
setStatus("PNG のパラメータを読み込み中...");
const form = new FormData();
form.append("file", file);
const res = await fetch("/api/upload-image", {
method: "POST",
body: form
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const meta = await res.json();
if (meta.error) throw new Error(meta.error);
gridImages[CENTER_INDEX] = meta;
currentCenter = meta;
renderGrid();
fillInputsFromMeta(currentCenter);
updateButtonStates();
setStatus("PNG の設定を中央に読み込みました");
} else {
// Inpaint モード
// 1) PNG + PT の組み合わせ → inpaint-upload
// 2) 透過PNG1枚 → マスクとして読み込み
let hasPng = false, hasPt = false;
for (let i = 0; i < files.length; i++) {
const f = files[i];
const name = f.name.toLowerCase();
if (name.endsWith(".png")) hasPng = true;
if (name.endsWith(".pt")) hasPt = true;
}
if (hasPng && hasPt) {
setStatus("Inpaint画像+latent を読み込み中...");
const form = new FormData();
for (let i = 0; i < files.length; i++) {
form.append("files", files[i]);
}
const res = await fetch("/api/inpaint-upload", {
method: "POST",
body: form
});
if (!res.ok) {
const errText = await res.text();
throw new Error("HTTP " + res.status + ": " + errText);
}
const meta = await res.json();
if (meta.error) throw new Error(meta.error);
currentInpaint = {
imageInfo: meta,
latentFilename: meta.latent_filename || null
};
inpaintImageEl.src = meta.url;
inpaintImageEl.onload = () => {
resizeMaskCanvasToImage();
clearMask();
};
fillInputsFromMeta(meta);
setStatus("Inpaint 画像+latent を読み込みました");
} else if (hasPng && !hasPt && files.length === 1) {
// マスクPNGとみなして maskCanvas に描画
const file = files[0];
const imgUrl = URL.createObjectURL(file);
const maskImg = new Image();
maskImg.onload = () => {
// キャンバスサイズに合わせて描画
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
maskCtx.globalAlpha = 0.6;
maskCtx.drawImage(maskImg, 0, 0, maskCanvas.width, maskCanvas.height);
maskCtx.globalAlpha = 1.0;
URL.revokeObjectURL(imgUrl);
setStatus("マスクPNGを読み込みました");
};
maskImg.src = imgUrl;
} else {
setStatus("Inpaint モードでは、PNG+PT か、1枚のマスクPNGをドロップしてください");
}
}
} catch (err) {
console.error(err);
setStatus("ドロップ処理中にエラーが発生しました: " + err.message);
}
});
// ---- イベントリスナー ----
generateOneBtn.addEventListener("click", handleGenerateOne);
generateGridBtn.addEventListener("click", handleGenerateGrid);
generateAutoOneBtn.addEventListener("click", startAutoGenerateOne);
generateAutoGridBtn.addEventListener("click", startAutoGenerateGrid);
stopAutoBtn.addEventListener("click", stopAll);
searchBtn.addEventListener("click", handleSearch);
inpaintBtn.addEventListener("click", enterInpaintMode);
inpaintNormalBtn.addEventListener("click", leaveInpaintMode);
repaintBtn.addEventListener("click", handleRepaint);
resetInpaintBtn.addEventListener("click", resetInpaintToInitial);
i2iBtn.addEventListener("click", handleI2I);
penBtn.addEventListener("click", () => { drawMode = "pen"; setStatus("ペンモード"); });
eraserBtn.addEventListener("click", () => { drawMode = "eraser"; setStatus("消しゴムモード"); });
clearMaskBtn.addEventListener("click", () => { clearMask(); setStatus("マスクをクリアしました"); });
maskColorSelect.addEventListener("change", () => {
setMaskColor(maskColorSelect.value);
});
// mask canvas mouse events
maskCanvas.addEventListener("mousedown", startDrawing);
maskCanvas.addEventListener("mousemove", drawOnMask);
window.addEventListener("mouseup", stopDrawing);
maskCanvas.addEventListener("mouseleave", stopDrawing);
// 初期状態
renderGrid();
updateButtonStates();
setMaskColor("red");
</script>
</body>
</html>