| <!doctype html> |
| <html lang="ja"> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| <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; |
| } |
| |
| #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> |
|
|
| |
| <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; |
| let inpaintInitialState = null; |
| |
| |
| let drawing = false; |
| let drawMode = "pen"; |
| 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; |
| |
| |
| |
| |
| |
| 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 { |
| |
| const rwRes = await fetch("/api/random-word"); |
| let randomWord = "extra"; |
| if (rwRes.ok) { |
| const rwJson = await rwRes.json(); |
| if (rwJson.word) randomWord = rwJson.word; |
| } |
| |
| |
| let stMinus = baseSteps + randInt(-15, -1); |
| let stPlus = baseSteps + randInt(1, 15); |
| stMinus = Math.max(1, stMinus); |
| stPlus = Math.max(1, stPlus); |
| |
| |
| 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); |
| |
| |
| 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(","); |
| } |
| |
| |
| 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 { |
| |
| promptShuffleRight = promptShuffleLeft; |
| } |
| } |
| |
| |
| const promptRemovedBottom = promptRemovedOnce(); |
| |
| |
| const promptWord = basePrompt ? (randomWord +"," + basePrompt) : randomWord; |
| |
| const neighborConfigs = [ |
| |
| { index: 1, prompt: basePrompt, steps: stMinus, guidance_scale: baseGuidanceScale }, |
| { index: 7, prompt: basePrompt, steps: stPlus, guidance_scale: baseGuidanceScale }, |
| |
| { index: 3, prompt: basePrompt, steps: baseSteps, guidance_scale: gsMinus }, |
| { index: 5, prompt: basePrompt, steps: baseSteps, guidance_scale: gsPlus }, |
| |
| { index: 0, prompt: promptShuffleLeft, steps: baseSteps, guidance_scale: baseGuidanceScale }, |
| { index: 2, prompt: promptShuffleRight, steps: baseSteps, guidance_scale: baseGuidanceScale }, |
| |
| { index: 6, prompt: promptRemovedBottom, steps: baseSteps, guidance_scale: baseGuidanceScale }, |
| |
| { index: 8, prompt: promptWord, steps: baseSteps, guidance_scale: baseGuidanceScale } |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| |
| function enterInpaintMode() { |
| if (!currentCenter) { |
| setStatus("Inpaint: まず中央画像を選択してください"); |
| return; |
| } |
| inpaintMode = true; |
| normalModeEl.style.display = "none"; |
| inpaintModeEl.style.display = "block"; |
| isManualGenerating = false; |
| isAutoGenerating = false; |
| stopRequested = false; |
| |
| |
| 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(); |
| }; |
| |
| |
| 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(); |
| |
| 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; |
| } |
| |
| 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 { |
| |
| 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); |
| } |
| |
| 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; |
| |
| |
| let pct = parseFloat(i2iStrengthEl.value); |
| if (Number.isNaN(pct)) pct = 100; |
| pct = Math.max(0, Math.min(100, pct)); |
| |
| |
| 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); |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| |
| 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) { |
| |
| 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 { |
| |
| |
| |
| 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) { |
| |
| 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); |
| }); |
| |
| |
| maskCanvas.addEventListener("mousedown", startDrawing); |
| maskCanvas.addEventListener("mousemove", drawOnMask); |
| window.addEventListener("mouseup", stopDrawing); |
| maskCanvas.addEventListener("mouseleave", stopDrawing); |
| |
| |
| renderGrid(); |
| updateButtonStates(); |
| setMaskColor("red"); |
| </script> |
| </body> |
| </html> |
|
|