1529 lines
62 KiB
Python
1529 lines
62 KiB
Python
import asyncio
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from io import BytesIO
|
|
|
|
import cv2
|
|
import requests
|
|
import torch
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
from aiohttp import web
|
|
from aiohttp.web_exceptions import HTTPForbidden, HTTPError
|
|
|
|
import server
|
|
import folder_paths
|
|
import comfy
|
|
|
|
wd14tagger = __import__("ComfyUI-WD14-Tagger").wd14tagger
|
|
|
|
checkpoints_list = folder_paths.get_filename_list("checkpoints")
|
|
|
|
allowed_ips = ["127.0.0.1"]
|
|
param = {"prompt": "", "negativePrompt": "", "seed": 0, "keepSeed": 0, "steps": 13, "cfg": 4, "interval": 30, "sampler": "dpmpp_2m,sgm_uniform", "checkpoint": "", "lora": "", "startstep": 0, "frames": 1, "framewait": 1, "videosrc": "", "videofst": 0, "videomax": 0, "videoskp": 8, "videostr": 1, "offsetX": 0, "offsetY": 0, "scale": 100}
|
|
state = {"mode": "", "width": 512, "height": 512, "autoUpdate": False, "presetTitle": time.strftime("%Y%m%d-%H%M"), "presetFolder": "", "presetFile": "", "loraRate": "1", "loraRank": "0", "checkpointFolder": "", "loraFolder": "", "loraFile": "", "loraTagOptions": "[]", "loraTag": "", "loraLinkHref": "", "loraPreviewSrc": "", "wd14th": 0.35, "wd14cth": 0.85}
|
|
frame_updating = False
|
|
frame_buffer = []
|
|
exclude_tags = ""
|
|
|
|
|
|
class AnyType(str):
|
|
def __ne__(self, __value: object) -> bool:
|
|
return False
|
|
|
|
any = AnyType("*")
|
|
|
|
HEAD=r"""
|
|
<head>
|
|
<style type="text/css">
|
|
|
|
html {
|
|
background-color: black;
|
|
}
|
|
|
|
body {
|
|
color: lightgray;
|
|
background: url(/stream) no-repeat top center local;
|
|
margin: 4px;
|
|
padding: 0px;
|
|
}
|
|
|
|
textarea {
|
|
color: lightgray;
|
|
background-color: black;
|
|
width: 100%;
|
|
font-size: 100%;
|
|
}
|
|
|
|
input {
|
|
color: lightgray;
|
|
background-color: black;
|
|
font-size: 100%;
|
|
}
|
|
|
|
button {
|
|
color: black;
|
|
background-color: dimgray;
|
|
font-size: 100%;
|
|
border-radius: 4px;
|
|
padding: 0px;
|
|
min-width: 2em;
|
|
}
|
|
|
|
button.tags {
|
|
font-size: 120%;
|
|
padding: 2px;
|
|
min-width: 2em;
|
|
}
|
|
|
|
button.disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
select {
|
|
color: lightgray;
|
|
background-color: black;
|
|
font-size: 100%;
|
|
}
|
|
|
|
div.row {
|
|
display: flex;
|
|
}
|
|
|
|
#leftPanel, #captureLeftPanel, #tagLeftPanel,
|
|
#rightPanel, #captureRightPanel, #tagRightPanel {
|
|
width: 12%;
|
|
}
|
|
|
|
#centerPanel, #captureCenterPanel, #tagCenterPanel {
|
|
width: 76%;
|
|
text-align: center;
|
|
}
|
|
|
|
#captureDialog, #tagDialog {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 999;
|
|
}
|
|
|
|
#samplerSelect,
|
|
#checkpointFolderSelect,
|
|
#moveCheckpointSelect,
|
|
#presetFolderSelect,
|
|
#presetFileSelect,
|
|
#movePresetSelect,
|
|
#loraFolderSelect,
|
|
#moveLoraSelect {
|
|
width: 100%;
|
|
}
|
|
|
|
#moveCheckpointSelect,
|
|
#movePresetSelect,
|
|
#moveLoraSelect {
|
|
display: none;
|
|
}
|
|
|
|
#seedInput,
|
|
#keepSeedInput,
|
|
#checkpointFileSelect,
|
|
#presetTitleInput,
|
|
#loraFileSelect,
|
|
#loraTagSelect,
|
|
#videoSrcSelect {
|
|
width: 80%;
|
|
}
|
|
|
|
#stepsRange,
|
|
#cfgRange,
|
|
#startstepRange,
|
|
#framesRange,
|
|
#framewaitRange,
|
|
#videoStrRange,
|
|
#videoFstRange,
|
|
#videoSkpRange,
|
|
#wd14thRange,
|
|
#wd14cthRange {
|
|
width: 50%;
|
|
}
|
|
|
|
#toggleView {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
#loraLink img {
|
|
max-width: 100%;
|
|
max-height: 8em;
|
|
width: auto;
|
|
height: auto;
|
|
margin: auto;
|
|
display: block;
|
|
}
|
|
|
|
#offsetXRange,
|
|
#offsetYRange,
|
|
#scaleRange {
|
|
width: 80%;
|
|
}
|
|
|
|
</style>
|
|
<title>FlipStreamViewer</title>
|
|
</head>
|
|
"""
|
|
|
|
SCRIPT_PARAM=r"""
|
|
function getStateAsJson() {
|
|
const autoUpdate = document.getElementById("autoUpdateCheckbox").checked;
|
|
const presetTitle = document.getElementById("presetTitleInput").value;
|
|
const presetFolder = document.getElementById("presetFolderSelect").value;
|
|
const presetFile = document.getElementById("presetFileSelect").value;
|
|
const checkpointFolder = document.getElementById("checkpointFolderSelect").value;
|
|
const loraFolder = document.getElementById("loraFolderSelect").value;
|
|
const loraFile = document.getElementById("loraFileSelect").value;
|
|
const loraTagSelect = document.getElementById("loraTagSelect");
|
|
const loraTagOptions = JSON.stringify([...loraTagSelect.options].map(o => ({ value: o.value, text: o.text })));
|
|
const loraTag = document.getElementById("loraTagSelect").value;
|
|
const loraRate = document.getElementById("loraRate").value;
|
|
const loraRank = document.getElementById("loraRank").value;
|
|
const loraLinkHref = document.getElementById("loraLink").getAttribute("href");
|
|
const loraPreviewSrc = document.getElementById("loraPreview").getAttribute("src");
|
|
const wd14th = parseFloat(document.getElementById("wd14thRange").value);
|
|
const wd14cth = parseFloat(document.getElementById("wd14cthRange").value);
|
|
return { autoUpdate: autoUpdate, presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, checkpointFolder: checkpointFolder, loraRate: loraRate, loraRank: loraRank, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, wd14th: wd14th, wd14cth: wd14cth };
|
|
}
|
|
|
|
function getParamAsJson() {
|
|
const prompt = document.getElementById("promptInput").value.trim();
|
|
const negativePrompt = document.getElementById("negativePromptInput").value.trim();
|
|
const seed = parseInt(document.getElementById("seedInput").value) || 0;
|
|
const keepSeed = parseInt(document.getElementById("keepSeedInput").value) || 0;
|
|
const steps = parseInt(document.getElementById("stepsRange").value);
|
|
const cfg = parseFloat(document.getElementById("cfgRange").value);
|
|
const interval = parseInt(document.getElementById("intervalSelect").value);
|
|
const sampler = document.getElementById("samplerSelect").value;
|
|
const checkpoint = document.getElementById("checkpointFileSelect").value;
|
|
const lora = document.getElementById("loraInput").value.trim();
|
|
const startstep = parseInt(document.getElementById("startstepRange").value);
|
|
const frames = parseInt(document.getElementById("framesRange").value);
|
|
const framewait = parseFloat(document.getElementById("framewaitRange").value);
|
|
const videosrc = document.getElementById("videoSrcSelect").value;
|
|
const videofst = parseInt(document.getElementById("videoFstRange").value);
|
|
const videomax = parseInt(document.getElementById("videoFstRange").getAttribute("max"));
|
|
const videoskp = parseInt(document.getElementById("videoSkpRange").value);
|
|
const videostr = parseFloat(document.getElementById("videoStrRange").value);
|
|
const offsetX = parseInt(document.getElementById("offsetXRange").value);
|
|
const offsetY = parseInt(document.getElementById("offsetYRange").value);
|
|
const scale = parseInt(document.getElementById("scaleRange").value);
|
|
return { prompt: prompt, negativePrompt: negativePrompt, seed: seed, keepSeed: keepSeed, steps: steps, cfg: cfg, interval: interval, sampler: sampler, checkpoint: checkpoint, lora: lora, startstep: startstep, frames: frames, framewait: framewait, videosrc: videosrc, videofst: videofst, videomax: videomax, videoskp: videoskp, videostr: videostr, offsetX: offsetX, offsetY: offsetY, scale: scale };
|
|
}
|
|
|
|
function setParam(param) {
|
|
document.getElementById("promptInput").value = param.prompt || "";
|
|
document.getElementById("negativePromptInput").value = param.negativePrompt || "";
|
|
document.getElementById("seedInput").value = param.seed || 0;
|
|
document.getElementById("keepSeedInput").value = param.keepSeed || 0;
|
|
document.getElementById("stepsRange").value = param.steps || 13;
|
|
document.getElementById("stepsValue").innerText = param.steps || 13;
|
|
document.getElementById("cfgRange").value = param.cfg || 4;
|
|
document.getElementById("cfgValue").innerText = param.cfg || 4;
|
|
document.getElementById("intervalSelect").value = param.interval || 30;
|
|
document.getElementById("samplerSelect").value = param.sampler || "dpmpp_2m,sgm_uniform";
|
|
document.getElementById("checkpointFileSelect").value = param.checkpoint || "";
|
|
document.getElementById("loraInput").value = param.lora || "";
|
|
document.getElementById("startstepRange").value = param.startstep || 0;
|
|
document.getElementById("startstepValue").innerText = param.startstep || 0;
|
|
document.getElementById("framesRange").value = param.frames || 1;
|
|
document.getElementById("framesValue").innerText = param.frames || 1;
|
|
document.getElementById("framewaitRange").value = param.framewait || 1;
|
|
document.getElementById("framewaitValue").innerText = param.framewait || 1;
|
|
document.getElementById("videoSrcSelect").value = param.videosrc || "";
|
|
document.getElementById("videoFstRange").setAttribute("max", param.videomax || 0);
|
|
document.getElementById("videoFstRange").value = param.videofst || 0;
|
|
document.getElementById("videoFstValue").innerText = param.videofst || 0;
|
|
document.getElementById("videoSkpRange").value = param.videoskp || 8;
|
|
document.getElementById("videoSkpValue").innerText = param.videoskp || 8;
|
|
document.getElementById("videoStrRange").value = param.videostr || 1;
|
|
document.getElementById("videoStrValue").innerText = param.videostr || 1;
|
|
document.getElementById("offsetXRange").value = param.offsetX || 0;
|
|
document.getElementById("offsetYRange").value = param.offsetY || 0;
|
|
document.getElementById("scaleRange").value = param.scale || 100;
|
|
}
|
|
|
|
function updateParam(reload=false) {
|
|
var updateButton = document.getElementById("updateButton");
|
|
updateButton.disabled = true;
|
|
fetch("/update_param", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), getParamAsJson()]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
if (reload) {
|
|
location.reload();
|
|
}
|
|
} else {
|
|
alert("Failed to update parameter.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while updating parameter.");
|
|
});
|
|
updateButton.disabled = false;
|
|
}
|
|
"""
|
|
|
|
SCRIPT_PRESET=r"""
|
|
function loadPreset(loraPromptOnly=false) {
|
|
fetch("/load_preset", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), loraPromptOnly]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => response.json()).then(json => {
|
|
if (loraPromptOnly) {
|
|
document.getElementById("loraInput").value = json.lora;
|
|
document.getElementById("presetTitleInput").value = json.presetTitle;
|
|
}
|
|
else {
|
|
location.reload();
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while loading preset: " + error);
|
|
});
|
|
}
|
|
|
|
function randomPreset() {
|
|
const presetFileSelect = document.getElementById("presetFileSelect");
|
|
presetFileSelect.selectedIndex = Math.floor(Math.random() * (presetFileSelect.options.length - 1)) + 1;
|
|
}
|
|
|
|
function movePreset() {
|
|
document.getElementById("movePresetSelect").style.display = "block";
|
|
document.getElementById("movePresetSelect").value = "";
|
|
}
|
|
|
|
function movePresetFile() {
|
|
const moveFile = document.getElementById("presetFileSelect").value;
|
|
const moveTo = document.getElementById("movePresetSelect").value;
|
|
if (!moveFile || !moveTo) {
|
|
return;
|
|
}
|
|
fetch("/move_presetfile", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), moveTo]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
alert("Preset moved successfully!");
|
|
} else {
|
|
alert("Failed to move preset.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while moving preset.");
|
|
});
|
|
}
|
|
|
|
function savePreset() {
|
|
const title = document.getElementById("presetTitleInput").value;
|
|
if (title == "") {
|
|
alert("Please enter a title to save.");
|
|
return;
|
|
}
|
|
if (!/^[a-zA-Z0-9-_]+$/.test(title)) {
|
|
alert("Title can only contain [a-zA-Z0-9-_] characters.");
|
|
return;
|
|
}
|
|
fetch("/save_preset", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), getParamAsJson()]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
alert("Preset saved successfully!");
|
|
} else {
|
|
alert("Failed to save preset.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while saving preset.");
|
|
});
|
|
}
|
|
"""
|
|
|
|
SCRIPT_CKPT=r"""
|
|
function moveCheckpoint() {
|
|
document.getElementById("moveCheckpointSelect").style.display = "block";
|
|
document.getElementById("moveCheckpointSelect").value = "";
|
|
}
|
|
|
|
function moveCheckpointFile() {
|
|
const moveFrom = document.getElementById("checkpointFileSelect").value;
|
|
const moveTo = document.getElementById("moveCheckpointSelect").value;
|
|
if (!moveFrom || !moveTo) {
|
|
return;
|
|
}
|
|
fetch("/move_checkpointfile", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), moveFrom, moveTo]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
alert("Checkpoint moved successfully!");
|
|
} else {
|
|
alert("Failed to move checkpoint.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while moving checkpoint.");
|
|
});
|
|
}
|
|
"""
|
|
|
|
SCRIPT_LORA=r"""
|
|
function selectLoraFile() {
|
|
const loraFolder = document.getElementById("loraFolderSelect").value;
|
|
const loraFileSelect = document.getElementById("loraFileSelect");
|
|
const loraName = loraFileSelect.options[loraFileSelect.selectedIndex].text;
|
|
const loraFile = loraFileSelect.value;
|
|
const loraRate = document.getElementById("loraRate").value;
|
|
const loraTagSelect = document.getElementById("loraTagSelect");
|
|
const loraInput = document.getElementById("loraInput");
|
|
const loraLink = document.getElementById("loraLink");
|
|
const loraPreview = document.getElementById("loraPreview");
|
|
|
|
if (loraFile) {
|
|
const re = new RegExp("[\\n]?<lora:" + loraName + ":[-]?[0-9.]+>", "g");
|
|
if (loraInput.value.search(re) == -1) {
|
|
loraInput.value += "\n<lora:" + loraName + ":" + loraRate + ">";
|
|
}
|
|
}
|
|
|
|
fetch("/get_lorainfo", {
|
|
method: "POST",
|
|
body: JSON.stringify({loraFolder: loraFolder, loraFile: loraFile}),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => response.json()).then(json => {
|
|
loraTagSelect.innerHTML = "";
|
|
|
|
item = document.createElement("option");
|
|
item.value = "";
|
|
item.text = "tags";
|
|
loraTagSelect.appendChild(item);
|
|
|
|
if (json.ss_tag_frequency) {
|
|
const datasets = JSON.parse(json.ss_tag_frequency);
|
|
const tags = {};
|
|
for (const setName in datasets) {
|
|
const set = datasets[setName];
|
|
for (const t in set) {
|
|
if (t in tags) {
|
|
tags[t] += set[t];
|
|
} else {
|
|
tags[t] = set[t];
|
|
}
|
|
}
|
|
}
|
|
const sorted_tags = Object.entries(tags).sort((a, b) => b[1] - a[1]);
|
|
for (const i in sorted_tags) {
|
|
item = document.createElement("option");
|
|
item.value = sorted_tags[i][0].trim();
|
|
item.text = sorted_tags[i][0].trim() + ":" + sorted_tags[i][1];
|
|
loraTagSelect.appendChild(item);
|
|
}
|
|
}
|
|
|
|
loraLink.href = json._lorapreview_href || "";
|
|
loraPreview.src = json._lorapreview_src || "";
|
|
});
|
|
}
|
|
|
|
function toggleLora() {
|
|
const loraFileSelect = document.getElementById("loraFileSelect");
|
|
const loraName = loraFileSelect.options[loraFileSelect.selectedIndex].text;
|
|
const loraFile = loraFileSelect.value;
|
|
const loraRate = document.getElementById("loraRate").value;
|
|
const loraInput = document.getElementById("loraInput");
|
|
if (loraFile) {
|
|
const re = new RegExp("[\\n]?<lora:" + loraName + ":[-]?[0-9.]+>", "g");
|
|
if (loraInput.value.search(re) >= 0) {
|
|
loraInput.value = loraInput.value.replace(re, "");
|
|
} else {
|
|
loraInput.value += "\n<lora:" + loraName + ":" + loraRate + ">";
|
|
}
|
|
}
|
|
}
|
|
|
|
function moveLora() {
|
|
document.getElementById("moveLoraSelect").style.display = "block";
|
|
document.getElementById("moveLoraSelect").value = "";
|
|
}
|
|
|
|
function moveLoraFile() {
|
|
const moveFile = document.getElementById("loraFileSelect").value;
|
|
const moveTo = document.getElementById("moveLoraSelect").value;
|
|
if (!moveFile || !moveTo) {
|
|
return;
|
|
}
|
|
fetch("/move_lorafile", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), moveTo]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
alert("Lora moved successfully!");
|
|
} else {
|
|
alert("Failed to move lora.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while moving lora.");
|
|
});
|
|
}
|
|
"""
|
|
|
|
SCRIPT_VID=r"""
|
|
function selectVideoSrc() {
|
|
const videoFstRange = document.getElementById("videoFstRange");
|
|
const videoFstValue = document.getElementById("videoFstValue");
|
|
videoFstRange.setAttribute("max", 0);
|
|
videoFstRange.value = 0;
|
|
videoFstValue.innerText = 0;
|
|
}
|
|
|
|
function previewVideo() {
|
|
const videoFstRange = document.getElementById("videoFstRange");
|
|
const videoFstValue = document.getElementById("videoFstValue");
|
|
const videosrc = document.getElementById("videoSrcSelect").value;
|
|
const videofst = parseInt(document.getElementById("videoFstRange").value);
|
|
const videoskp = parseInt(document.getElementById("videoSkpRange").value);
|
|
const frames = parseInt(document.getElementById("framesRange").value);
|
|
if (!videosrc) {
|
|
videoFstRange.setAttribute("max", 0);
|
|
videoFstRange.value = 0;
|
|
videoFstValue.innerText = 0;
|
|
return;
|
|
}
|
|
fetch("/preview_video", {
|
|
method: "POST",
|
|
body: JSON.stringify([videosrc, videofst, videoskp, frames]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => response.json()).then(json => {
|
|
videoFstRange.setAttribute("max", json.total_frames - 1);
|
|
videoFstRange.value = Math.min(videofst, json.total_frames - 1);
|
|
videoFstValue.innerText = videoFstRange.value;
|
|
}).catch(error => {
|
|
alert("An error occurred while preview video.");
|
|
});
|
|
}
|
|
"""
|
|
|
|
SCRIPT_TAG=r"""
|
|
function toggleTag() {
|
|
const loraTagSelect = document.getElementById("loraTagSelect");
|
|
const loraTag = document.getElementById("loraTagSelect").value;
|
|
const loraRank = parseInt(document.getElementById("loraRank").value);
|
|
const loraInput = document.getElementById("loraInput");
|
|
if (loraTag) {
|
|
const re = new RegExp("^\\s*" + loraTag + "\\s*(,\\s*|\n|$)|,\\s*" + loraTag + "\\s*(,|\n|$)", "g");
|
|
if (loraInput.value.search(re) >= 0) {
|
|
loraInput.value = loraInput.value.replace(re, "$2")
|
|
} else {
|
|
if (loraInput.value) {
|
|
loraInput.value += ", " + loraTag;
|
|
} else {
|
|
loraInput.value = loraTag;
|
|
}
|
|
}
|
|
} else {
|
|
for (var i = 1; i < Math.min(loraRank + 1, loraTagSelect.length); i++) {
|
|
loraInput.value += ", " + loraTagSelect.options[i].value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function randomTag() {
|
|
const options = document.getElementById("loraTagSelect").options;
|
|
const loraInput = document.getElementById("loraInput");
|
|
const loraTag = options[Math.floor(Math.random() * options.length)].value;
|
|
if (loraTag) {
|
|
const re = new RegExp("^\\s*" + loraTag + "\\s*(,\\s*|\n|$)|,\\s*" + loraTag + "\\s*(,|\n|$)", "g");
|
|
if (loraInput.value.search(re) < 0) {
|
|
if (loraInput.value) {
|
|
loraInput.value += ", " + loraTag;
|
|
} else {
|
|
loraInput.value = loraTag;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function addWD14Tag() {
|
|
fetch("/get_wd14tag", {
|
|
method: "POST",
|
|
body: JSON.stringify(getStateAsJson()),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => response.json()).then(json => {
|
|
loraInput.value += "\n" + json.tags + "\n";
|
|
}).catch(error => {
|
|
alert(`Failed to fetch WD14 tags: ${error.stack}`);
|
|
});
|
|
}
|
|
|
|
function clearLoraInput() {
|
|
document.getElementById("loraInput").value = "";
|
|
}
|
|
|
|
function showTagDialog() {
|
|
const lora = document.getElementById("loraInput").value;
|
|
const tags = lora.replace(/\n/g, ',').split(',').map(value => value.trim());
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
|
|
for (const button of Array.from(tagCenterPanel.children)) {
|
|
if (!button.classList.contains("disabled")) {
|
|
button.classList.add("disabled");
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < tags.length; i++) {
|
|
if (!tags[i])
|
|
continue;
|
|
|
|
const child = Array.from(tagCenterPanel.children).find(child => child.value == tags[i]);
|
|
if (child)
|
|
{
|
|
child.classList.remove("disabled");
|
|
continue;
|
|
}
|
|
|
|
const button = document.createElement("button");
|
|
button.value = tags[i];
|
|
button.textContent = tags[i];
|
|
button.classList.add("tags");
|
|
button.addEventListener("click", function() {
|
|
if (button.classList.contains("disabled")) {
|
|
button.classList.remove("disabled");
|
|
} else {
|
|
button.classList.add("disabled");
|
|
}
|
|
});
|
|
tagCenterPanel.appendChild(button);
|
|
}
|
|
|
|
document.getElementById("tagDialog").style.display = "flex";
|
|
}
|
|
|
|
function tagSort() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
Array.from(tagCenterPanel.children).sort((a, b) => a.value > b.value ? 1 : -1).forEach(node => tagCenterPanel.appendChild(node));
|
|
}
|
|
|
|
function tagRSort() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
tagCenterPanel.children = Array.from(tagCenterPanel.children).sort((a, b) => Math.random() - 0.5).forEach(node => tagCenterPanel.appendChild(node));
|
|
}
|
|
|
|
function tagRSel() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
Array.from(tagCenterPanel.children).forEach(child => Math.random() < 0.5 && child.click());
|
|
}
|
|
|
|
function tagCSel() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
Array.from(tagCenterPanel.children).forEach(child => child.classList.contains("disabled") || child.classList.add("disabled"));
|
|
}
|
|
|
|
function tagOK() {
|
|
const tagButtons = Array.from(tagCenterPanel.children);
|
|
let selectedTags = [];
|
|
for (let i = 0; i < tagButtons.length; i++) {
|
|
if (!tagButtons[i].classList.contains("disabled")) {
|
|
selectedTags.push(tagButtons[i].value);
|
|
}
|
|
}
|
|
loraInput.value = selectedTags.join(", ");
|
|
closeTagDialog();
|
|
}
|
|
|
|
function tagOK() {
|
|
const tagButtons = Array.from(tagCenterPanel.children);
|
|
let selectedTags = [];
|
|
for (let i = 0; i < tagButtons.length; i++) {
|
|
if (!tagButtons[i].classList.contains("disabled")) {
|
|
selectedTags.push(tagButtons[i].value);
|
|
}
|
|
}
|
|
loraInput.value = selectedTags.join(", ");
|
|
closeTagDialog();
|
|
}
|
|
|
|
function closeTagDialog() {
|
|
document.getElementById("tagDialog").style.display = "none";
|
|
}
|
|
"""
|
|
|
|
SCRIPT_SEED=r"""
|
|
function changeSeed(reload=false) {
|
|
const seedInput = document.getElementById("seedInput");
|
|
seedInput.value = Math.floor(Math.random() * 99999);
|
|
updateParam(reload);
|
|
}
|
|
|
|
function keepSeed() {
|
|
const seed = parseInt(document.getElementById("seedInput").value) || 0;
|
|
const keepSeedInput = document.getElementById("keepSeedInput");
|
|
keepSeedInput.value = seed;
|
|
}
|
|
|
|
function backSeed() {
|
|
const keepSeed = parseInt(document.getElementById("keepSeedInput").value) || 0;
|
|
const seedInput = document.getElementById("seedInput");
|
|
seedInput.value = keepSeed;
|
|
}
|
|
|
|
var intervalId = 0;
|
|
function toggleAutoUpdate() {
|
|
const autoUpdate = document.getElementById("autoUpdateCheckbox").checked;
|
|
const interval = parseInt(document.getElementById("intervalSelect").value);
|
|
if (autoUpdate) {
|
|
intervalId = setInterval(function() {
|
|
changeSeed();
|
|
}, interval * 1000);
|
|
} else {
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = 0;
|
|
}
|
|
}
|
|
}
|
|
toggleAutoUpdate();
|
|
"""
|
|
|
|
SCRIPT_VIEW=r"""
|
|
var toggleViewFlag = true;
|
|
function toggleView() {
|
|
const leftPanel = document.getElementById("leftPanel");
|
|
const rightPanel = document.getElementById("rightPanel");
|
|
if (toggleViewFlag) {
|
|
leftPanel.style.visibility = "hidden";
|
|
rightPanel.style.visibility = "hidden";
|
|
toggleViewFlag = false;
|
|
} else {
|
|
document.body.style.backgroundImage = "none";
|
|
leftPanel.style.visibility = "visible";
|
|
rightPanel.style.visibility = "visible";
|
|
toggleViewFlag = true;
|
|
}
|
|
}
|
|
|
|
function hideTimer() {
|
|
setTimeout(function() {
|
|
document.getElementById("autoUpdateCheckbox").checked = false;
|
|
toggleAutoUpdate();
|
|
closeCaptureDialog();
|
|
document.body.style.backgroundImage = "none";
|
|
leftPanel.style.visibility = "hidden";
|
|
rightPanel.style.visibility = "hidden";
|
|
toggleViewFlag = false;
|
|
}, 300 * 1000);
|
|
}
|
|
hideTimer();
|
|
"""
|
|
|
|
SCRIPT_CAPTURE=r"""
|
|
let capturedCanvas = document.createElement("canvas");
|
|
async function capture() {
|
|
try {
|
|
const video = document.createElement("video");
|
|
video.srcObject = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
|
video.style.display = "none";
|
|
video.play();
|
|
video.addEventListener("loadedmetadata", () => {
|
|
const { videoWidth, videoHeight } = video;
|
|
capturedCtx = capturedCanvas.getContext("2d");
|
|
capturedCtx.clearRect(0, 0, capturedCanvas.width, capturedCanvas.height);
|
|
capturedCanvas.width = videoWidth;
|
|
capturedCanvas.height = videoHeight;
|
|
capturedCtx.drawImage(video, 0, 0);
|
|
video.srcObject.getTracks().forEach(track => track.stop());
|
|
video.srcObject = null;
|
|
updateCanvas();
|
|
});
|
|
} catch (error) {
|
|
alert("An error occurred while capture.");
|
|
}
|
|
}
|
|
|
|
function updateCanvas() {
|
|
document.getElementById("captureDialog").style.display = "flex";
|
|
const canvas = document.getElementById("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
const offsetX = parseInt(document.getElementById("offsetXRange").value, 10);
|
|
const offsetY = parseInt(document.getElementById("offsetYRange").value, 10);
|
|
const scale = parseInt(document.getElementById("scaleRange").value, 10) / 100;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.save();
|
|
ctx.scale(scale, scale);
|
|
ctx.translate(offsetX, offsetY);
|
|
ctx.drawImage(capturedCanvas, 0, 0);
|
|
ctx.restore();
|
|
}
|
|
|
|
function closeCaptureDialog() {
|
|
document.getElementById("captureDialog").style.display = "none";
|
|
}
|
|
|
|
function resetPos() {
|
|
document.getElementById("offsetXRange").value = 0;
|
|
document.getElementById("offsetYRange").value = 0;
|
|
document.getElementById("scaleRange").value = 100;
|
|
updateCanvas();
|
|
}
|
|
|
|
function setFrame() {
|
|
const canvas = document.getElementById("canvas");
|
|
const dataURL = canvas.toDataURL("image/webp");
|
|
fetch("/set_frame", {
|
|
method: "POST",
|
|
body: JSON.stringify([dataURL]),
|
|
headers: { "Content-Type": "application/json" }
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
closeCaptureDialog();
|
|
} else {
|
|
alert("Failed to set frame.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while set frame.");
|
|
});
|
|
}
|
|
"""
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/stream")
|
|
async def stream(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
response = web.StreamResponse()
|
|
response.content_type = "multipart/x-mixed-replace; boundary=frame"
|
|
await response.prepare(request)
|
|
|
|
update_start = None
|
|
while request.transport and not request.transport.is_closing():
|
|
if frame_buffer:
|
|
if len(frame_buffer) > 1:
|
|
frame = frame_buffer.pop(0)
|
|
frame_buffer.append(frame)
|
|
else:
|
|
frame = frame_buffer[0]
|
|
|
|
if frame_updating:
|
|
if update_start is None:
|
|
update_start = time.time()
|
|
with BytesIO(frame) as inbuf:
|
|
image = Image.open(inbuf)
|
|
w, h = image.size
|
|
s = (time.time() - update_start + 1) * 4
|
|
draw = ImageDraw.Draw(image)
|
|
draw.rectangle((int(w / 2 - s), 16, int(w / 2 + s), 32), fill="blue", outline="blue", width=1)
|
|
with BytesIO() as outbuf:
|
|
image.save(outbuf, format="WEBP", quality=100)
|
|
frame = outbuf.getvalue()
|
|
else:
|
|
update_start = None
|
|
|
|
await response.write(b"--frame\r\nContent-Type: image/webp\r\n\r\n" + frame + b"\r\n")
|
|
await response.write(b"--frame\r\nContent-Type: image/webp\r\n\r\n" + frame + b"\r\n")
|
|
|
|
if param["framewait"] > 0:
|
|
await asyncio.sleep(param["framewait"] * 0.1)
|
|
|
|
return response
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/viewer")
|
|
async def viewer(request):
|
|
if request.remote not in allowed_ips:
|
|
print(request.remote)
|
|
raise HTTPForbidden()
|
|
|
|
return web.Response(text=f"""<html>{HEAD}<body>
|
|
<div class="row">
|
|
<div id="leftPanel">
|
|
<textarea id="promptInput" placeholder="Enter prompt" rows="15">{param["prompt"]}</textarea>
|
|
<textarea id="negativePromptInput" placeholder="Enter negativePrompt" rows="2">{param["negativePrompt"]}</textarea>
|
|
<div class="row">
|
|
Seed: <input id="seedInput" type="number" placeholder="Enter seed" value="{param["seed"]}" />
|
|
</div>
|
|
<div class="row">
|
|
Keep: <input id="keepSeedInput" type="number" placeholder="keepSeed" value="{param["keepSeed"]}" />
|
|
</div>
|
|
<div class="row">
|
|
Iv:<select id="intervalSelect" onchange="updateParam()">
|
|
<option value="10" {"selected" if param["interval"] == 10 else ""}>10</option>
|
|
<option value="15" {"selected" if param["interval"] == 15 else ""}>15</option>
|
|
<option value="20" {"selected" if param["interval"] == 20 else ""}>20</option>
|
|
<option value="25" {"selected" if param["interval"] == 25 else ""}>25</option>
|
|
<option value="30" {"selected" if param["interval"] == 30 else ""}>30</option>
|
|
</select>
|
|
<input id="autoUpdateCheckbox" type="checkbox" onchange="toggleAutoUpdate()"{" checked" if state["autoUpdate"] else ""}>Auto</input>
|
|
</div>
|
|
<div class="row">
|
|
<button id="updateButton" class="willreload" onclick="updateParam(true)">U</button>
|
|
<button id="changeSeedButton" onclick="changeSeed()">R</button>
|
|
<button id="keepSeedButton" onclick="keepSeed()">K</button>
|
|
<button id="backSeedButton" onclick="backSeed()">B</button>
|
|
</div>
|
|
<div class="row">
|
|
<input id="stepsRange" type="range" min="1" max="50" step="1" value="{param["steps"]}" oninput="stepsValue.innerText = this.value;" />
|
|
<span id="stepsValue">{param["steps"]}</span>stp
|
|
</div>
|
|
<div class="row">
|
|
<input id="cfgRange" type="range" min="1.0" max="15.0" step="0.1" value="{param["cfg"]}" oninput="cfgValue.innerText = this.value;" />
|
|
<span id="cfgValue">{param["cfg"]}</span>cfg
|
|
</div>
|
|
<div class="row">
|
|
<input id="startstepRange" type="range" min="0" max="6" step="1" value="{param["startstep"]}" oninput="startstepValue.innerText = this.value;" />
|
|
<span id="startstepValue">{param["startstep"]}</span>sta
|
|
</div>
|
|
<div class="row">
|
|
<input id="framesRange" type="range" min="1" max="8" step="1" value="{param["frames"]}" oninput="framesValue.innerText = this.value;" />
|
|
<span id="framesValue">{param["frames"]}</span>frm
|
|
</div>
|
|
<div class="row">
|
|
<input id="framewaitRange" type="range" min="0" max="2" step="0.1" value="{param["framewait"]}" oninput="framewaitValue.innerText = this.value;" />
|
|
<span id="framewaitValue">{param["framewait"]}</span>spf
|
|
</div>
|
|
<select id="samplerSelect" onchange="updateParam()">
|
|
<option value="dpmpp_2m,sgm_uniform" {"selected" if param["sampler"] == "dpmpp_2m,sgm_uniform" else ""}>dpmpp_2m,sgm_uniform</option>
|
|
<option value="ddim,ddim_uniform" {"selected" if param["sampler"] == "ddim,ddim_uniform" else ""}>ddim,ddim_uniform</option>
|
|
<option value="eular,normal" {"selected" if param["sampler"] == "eular,normal" else ""}>eular,normal</option>
|
|
</select>
|
|
<select id="checkpointFolderSelect" class="willreload" onchange="updateParam(true)">
|
|
<option value="" disabled selected>checkpoint folder</option>
|
|
{"".join([f'<option value="{dir.name}"{" selected" if state["checkpointFolder"] == dir.name else ""}>{dir.name}</option>' for dir in Path("ComfyUI/models/checkpoints", state["mode"]).glob("*/")])}
|
|
</select>
|
|
<div class="row">
|
|
<select id="checkpointFileSelect">
|
|
<option value="" selected>checkpoint</option>
|
|
{f'<option value="{param["checkpoint"]}" selected>*{Path(param["checkpoint"]).stem}</option>' if param['checkpoint'] else ""}
|
|
{"".join([f'<option value="{file.relative_to("ComfyUI/models/checkpoints")}">{file.stem}</option>' if param["checkpoint"] != file.relative_to("ComfyUI/models/checkpoints") else "" for file in Path("ComfyUI/models/checkpoints", state["mode"], state["checkpointFolder"]).glob("*.safetensors")])}
|
|
</select>
|
|
<button id="moveCheckpointButton" onClick="moveCheckpoint()">M</button>
|
|
</div>
|
|
<select id="moveCheckpointSelect" onchange="moveCheckpointFile()">
|
|
<option value="" disabled selected>move to</option>
|
|
{"".join([f'<option value="{dir.name}">{dir.name}</option>' for dir in Path("ComfyUI/models/checkpoints", state["mode"]).glob("*/")])}
|
|
</select>
|
|
</div>
|
|
<div id="centerPanel">
|
|
<button id="toggleView" onclick="toggleView()"></button>
|
|
</div>
|
|
<div id="rightPanel">
|
|
<select id="loraFolderSelect" class="willreload" onchange="updateParam(true)">
|
|
<option value="" disabled selected>lora folder</option>
|
|
{"".join([f'<option value="{dir.name}"{" selected" if state["loraFolder"] == dir.name else ""}>{dir.name}</option>' for dir in Path("ComfyUI/models/loras", state["mode"]).glob("*/")])}
|
|
</select>
|
|
<div class="row">
|
|
<select id="loraFileSelect" onchange="selectLoraFile()">
|
|
<option value="" disabled selected>lora file</option>
|
|
{"".join([f'<option value="{file.name}"{" selected" if state["loraFile"] == file.name else ""}>{file.stem}</option>' for file in Path("ComfyUI/models/loras", state["mode"], state["loraFolder"]).glob("*.safetensors")])}
|
|
</select>
|
|
<button id="toggleLoraButton" onclick="toggleLora()">T</button>
|
|
<button id="moveLoraButton" onClick="moveLora()">M</button>
|
|
</div>
|
|
<select id="moveLoraSelect" onchange="moveLoraFile()">
|
|
<option value="" disabled selected>move to</option>
|
|
{"".join([f'<option value="{dir.name}">{dir.name}</option>' for dir in Path("ComfyUI/models/loras", state["mode"]).glob("*/")])}
|
|
</select>
|
|
<div class="row">
|
|
<select id="loraTagSelect" onchange="toggleTag()">
|
|
<option value="" disabled selected>tags</option>
|
|
{"".join([f'<option value="{opt["value"]}"{" selected" if state["loraTag"] == opt["value"] else ""}>{opt["text"]}</option>' if opt["value"] else "" for opt in json.loads(state["loraTagOptions"])])}
|
|
</select>
|
|
<button id="toggleTagButton" onclick="toggleTag()">T</button>
|
|
<button id="randomTagButton" onclick="randomTag()">R</button>
|
|
</div>
|
|
Rate: <select id="loraRate">
|
|
<option value="8" {"selected" if state["loraRate"] == "8" else ""}>8</option>
|
|
<option value="7" {"selected" if state["loraRate"] == "7" else ""}>7</option>
|
|
<option value="6" {"selected" if state["loraRate"] == "6" else ""}>6</option>
|
|
<option value="5" {"selected" if state["loraRate"] == "5" else ""}>5</option>
|
|
<option value="4" {"selected" if state["loraRate"] == "4" else ""}>4</option>
|
|
<option value="3" {"selected" if state["loraRate"] == "3" else ""}>3</option>
|
|
<option value="2" {"selected" if state["loraRate"] == "2" else ""}>2</option>
|
|
<option value="1" {"selected" if state["loraRate"] == "1" else ""}>1</option>
|
|
<option value="0.9" {"selected" if state["loraRate"] == "0.9" else ""}>0.9</option>
|
|
<option value="0.8" {"selected" if state["loraRate"] == "0.8" else ""}>0.8</option>
|
|
<option value="0.7" {"selected" if state["loraRate"] == "0.7" else ""}>0.7</option>
|
|
<option value="0.6" {"selected" if state["loraRate"] == "0.6" else ""}>0.6</option>
|
|
<option value="0.5" {"selected" if state["loraRate"] == "0.5" else ""}>0.5</option>
|
|
<option value="0.4" {"selected" if state["loraRate"] == "0.4" else ""}>0.4</option>
|
|
<option value="0.3" {"selected" if state["loraRate"] == "0.3" else ""}>0.3</option>
|
|
<option value="0.2" {"selected" if state["loraRate"] == "0.2" else ""}>0.2</option>
|
|
<option value="0.1" {"selected" if state["loraRate"] == "0.1" else ""}>0.1</option>
|
|
<option value="0" {"selected" if state["loraRate"] == "0" else ""}>0</option>
|
|
<option value="-1" {"selected" if state["loraRate"] == "-1" else ""}>-1</option>
|
|
<option value="-2" {"selected" if state["loraRate"] == "-2" else ""}>-2</option>
|
|
<option value="-3" {"selected" if state["loraRate"] == "-3" else ""}>-3</option>
|
|
<option value="-4" {"selected" if state["loraRate"] == "-4" else ""}>-4</option>
|
|
<option value="-5" {"selected" if state["loraRate"] == "-5" else ""}>-5</option>
|
|
</select>
|
|
<select id="loraRank">
|
|
<option value="0" {"selected" if state["loraRank"] == "0" else ""}>0</option>
|
|
<option value="10" {"selected" if state["loraRank"] == "10" else ""}>10</option>
|
|
<option value="20" {"selected" if state["loraRank"] == "20" else ""}>20</option>
|
|
<option value="30" {"selected" if state["loraRank"] == "30" else ""}>30</option>
|
|
<option value="40" {"selected" if state["loraRank"] == "40" else ""}>40</option>
|
|
<option value="50" {"selected" if state["loraRank"] == "50" else ""}>50</option>
|
|
</select>
|
|
<div class="row">
|
|
<select id="videoSrcSelect" onchange="selectVideoSrc()">
|
|
<option value="" selected>video source</option>
|
|
{"".join([f'<option value="{file.name}"{" selected" if param["videosrc"] == file.name else ""}>{file.stem}</option>' for file in Path("videosrc").glob("*.*")])}
|
|
</select>
|
|
<button id="previewVideoButton" onclick="previewVideo()">PV</button>
|
|
</div>
|
|
<div class="row">
|
|
<input id="videoFstRange" type="range" min="0" max="{param["videomax"]}" step="1" value="{param["videofst"]}" oninput="videoFstValue.innerText = this.value;" />
|
|
<span id="videoFstValue">{param["videofst"]}</span>fst
|
|
</div>
|
|
<div class="row">
|
|
<input id="videoSkpRange" type="range" min="1" max="32" step="1" value="{param["videoskp"]}" oninput="videoSkpValue.innerText = this.value;" />
|
|
<span id="videoSkpValue">{param["videoskp"]}</span>skp
|
|
</div>
|
|
<div class="row">
|
|
<input id="videoStrRange" type="range" min="0" max="2" step="0.01" value="{param["videostr"]}" oninput="videoStrValue.innerText = this.value;" />
|
|
<span id="videoStrValue">{param["videostr"]}</span>str
|
|
</div>
|
|
<select id="presetFolderSelect" class="willreload" onchange="updateParam(true)">
|
|
<option value="" disabled selected>preset folder</option>
|
|
{"".join([f'<option value="{dir.name}"{" selected" if state["presetFolder"] == dir.name else ""}>{dir.name}</option>' for dir in Path("preset").glob("*/")])}
|
|
</select>
|
|
<select id="presetFileSelect">
|
|
<option value="" disabled selected>preset</option>
|
|
{"".join([f'<option value="{file.name}"{" selected" if state["presetFile"] == file.name else ""}>{file.stem}</option>' for file in Path("preset", state["presetFolder"]).glob("*.json")])}
|
|
</select>
|
|
<div class="row">
|
|
<button id="randomPresetButton" onclick="randomPreset()">R</button>
|
|
<button id="loadPresetLoraPromptButton" onclick="loadPreset(true)">P</button>
|
|
<button id="loadPresetButton" class="willreload" onclick="loadPreset()">L</button>
|
|
<button id="movePresetButton" onclick="movePreset()">M</button>
|
|
</div>
|
|
<select id="movePresetSelect" onchange="movePresetFile()">
|
|
<option value="" disabled selected>move to</option>
|
|
{"".join([f'<option value="{dir.name}">{dir.name}</option>' for dir in Path("preset").glob("*/")])}
|
|
</select>
|
|
<div class="row">
|
|
<input id="presetTitleInput" placeholder="preset title" value="{state["presetTitle"]}" />
|
|
<button id="savePresetButton" onclick="savePreset()">Save</button>
|
|
</div>
|
|
<div class="row">
|
|
<button id="captureButton" onclick="capture()">C</button>
|
|
<button id="wd14Button" onclick="addWD14Tag()">W</button>
|
|
<button id="showTagDialogButton" onclick="showTagDialog()">T</button>
|
|
<button id="updateButton2" onclick="updateParam()">U</button>
|
|
<button id="clearLoraInputButton" onclick="clearLoraInput()">Clr</button>
|
|
</div>
|
|
<div class="row">
|
|
<input id="wd14thRange" type="range" min="0" max="1" step="0.01" value="{state["wd14th"]}" oninput="wd14thValue.innerText = this.value;" />
|
|
<span id="wd14thValue">{state["wd14th"]}</span>wth
|
|
</div>
|
|
<div class="row">
|
|
<input id="wd14cthRange" type="range" min="0" max="1" step="0.01" value="{state["wd14cth"]}" oninput="wd14cthValue.innerText = this.value;" />
|
|
<span id="wd14cthValue">{state["wd14cth"]}</span>cth
|
|
</div>
|
|
<textarea id="loraInput" placeholder="Enter lora" rows="15">{param["lora"]}</textarea>
|
|
<div class="row">
|
|
<a id="loraLink" href="{state["loraLinkHref"] or "javascript:void(0)"}" target="_blank">
|
|
<img id="loraPreview" src="{state["loraPreviewSrc"]}" alt onerror="this.onerror = null; this.src='';" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="captureDialog">
|
|
<div id="captureLeftPanel"></div>
|
|
<div id="captureCenterPanel">
|
|
<canvas id="canvas" width="960" height="600" />
|
|
</div>
|
|
<div id="captureRightPanel">
|
|
<button id="resetPosButton" onclick="resetPos()">Reset</button>
|
|
<br>
|
|
X <input id="offsetXRange" type="range" min="-960" max="960" value="{param["offsetX"]}" onchange="updateCanvas()">
|
|
<br>
|
|
Y <input id="offsetYRange" type="range" min="-600" max="600" value="{param["offsetY"]}" onchange="updateCanvas()">
|
|
<br>
|
|
S <input id="scaleRange" type="range" min="50" max="400" value="{param["scale"]}" onchange="updateCanvas()">
|
|
<br>
|
|
<button id="setframeButton" onclick="setFrame()">SetFrame</button>
|
|
<button id="closeCaptureButton" onclick="closeCaptureDialog()">Close</button>
|
|
</div>
|
|
</div>
|
|
<div id="tagDialog">
|
|
<div id="tagLeftPanel"></div>
|
|
<div id="tagCenterPanel"></div>
|
|
<div id="tagRightPanel">
|
|
<button id="tagSortButton" onclick="tagSort()">Sort</button>
|
|
<button id="tagRSortButton" onclick="tagRSort()">RSort</button>
|
|
<br>
|
|
<button id="tagRSelButton" onclick="tagRSel()">RSel</button>
|
|
<button id="tagCSelButton" onclick="tagCSel()">CSel</button>
|
|
<br>
|
|
<button id="tagOKButton" onclick="tagOK()">OK</button>
|
|
<button id="tagCancelButton" onclick="closeTagDialog()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
{SCRIPT_PARAM}
|
|
{SCRIPT_PRESET}
|
|
{SCRIPT_CKPT}
|
|
{SCRIPT_LORA}
|
|
{SCRIPT_VID}
|
|
{SCRIPT_TAG}
|
|
{SCRIPT_SEED}
|
|
{SCRIPT_VIEW}
|
|
{SCRIPT_CAPTURE}
|
|
</script>
|
|
</body></html>""", content_type="text/html")
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/update_param")
|
|
async def update_param(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, prm = await request.json()
|
|
state.update(stt)
|
|
param.update(prm)
|
|
return web.Response(status=200)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/get_lorainfo")
|
|
async def get_lorainfo(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
req = await request.json()
|
|
loraPath = Path("ComfyUI/models/loras", state["mode"], req["loraFolder"], req["loraFile"])
|
|
hash = None
|
|
with open(loraPath, "rb") as file:
|
|
header_size = int.from_bytes(file.read(8), "little", signed=False)
|
|
if header_size <= 0:
|
|
raise HTTPError("get_lorainfo: Invalid header size")
|
|
|
|
header = file.read(header_size)
|
|
if header_size <= 0:
|
|
raise HTTPError("get_lorainfo: Invalid header")
|
|
|
|
file.seek(0)
|
|
hash = hashlib.sha256(file.read()).hexdigest()
|
|
|
|
header_json = json.loads(header)
|
|
lorainfo = header_json["__metadata__"] if "__metadata__" in header_json else {}
|
|
|
|
if hash:
|
|
res = requests.get("https://civitai.com/api/v1/model-versions/by-hash/" + hash).json()
|
|
lorainfo["_lorapreview_href"] = "https://civitai.com/models/" + str(res["modelId"])
|
|
lorainfo["_lorapreview_src"] = res["images"][0]["url"]
|
|
|
|
return web.json_response(lorainfo)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/get_wd14tag")
|
|
async def get_wd14tag(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt = await request.json()
|
|
state.update(stt);
|
|
tags = []
|
|
if frame_buffer:
|
|
with BytesIO(frame_buffer[0]) as buf:
|
|
image = Image.open(buf)
|
|
tags = await wd14tagger.tag(image, "wd-v1-4-moat-tagger-v2.onnx", state["wd14th"], state["wd14cth"], exclude_tags)
|
|
return web.json_response({"tags": tags})
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/set_frame")
|
|
async def set_frame(request):
|
|
global frame_buffer
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
dataURL, = await request.json()
|
|
_, data = dataURL.split(",", 1)
|
|
frame_buffer = [base64.b64decode(data)]
|
|
return web.Response(status=200)
|
|
|
|
|
|
def load_video(path, maxwidth, height, videofst, videoskp, maxcount):
|
|
cap = cv2.VideoCapture(path)
|
|
if not cap.isOpened():
|
|
return [], 0
|
|
|
|
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
cap.set(cv2.CAP_PROP_POS_FRAMES, videofst)
|
|
buf = []
|
|
for i in range(maxcount):
|
|
for s in range(videoskp + 1):
|
|
if not cap.isOpened():
|
|
return buf, total_frames
|
|
if not cap.grab():
|
|
return buf, total_frames
|
|
_, frame = cap.retrieve()
|
|
if height != frame.shape[0]:
|
|
scale = height / frame.shape[0]
|
|
width = int(frame.shape[1] * scale // 8 * 8)
|
|
frame = cv2.resize(frame, (width, height), interpolation=cv2.INTER_LANCZOS4)
|
|
if width > maxwidth:
|
|
x = int(frame.shape[1] / 2 - maxwidth / 2)
|
|
frame = frame[:, x:x + maxwidth]
|
|
buf.append(frame)
|
|
return buf, total_frames
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/preview_video")
|
|
async def preview_video(request):
|
|
global frame_buffer
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
videosrc, videofst, videoskp, frames = await request.json()
|
|
buf, total_frames = load_video(str(Path("videosrc", videosrc)), state["width"], state["height"], videofst, videoskp, frames)
|
|
buf = [BytesIO(cv2.imencode(".webp", frame)[1]).getvalue() for frame in buf]
|
|
buf += buf[::-1]
|
|
frame_buffer = buf
|
|
return web.json_response({"total_frames": total_frames})
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/move_checkpointfile")
|
|
async def move_lorafile(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, moveFrom, moveTo = await request.json()
|
|
state.update(stt)
|
|
pathFrom = Path("ComfyUI/models/checkpoints", state["mode"], state["checkpointFolder"], Path(moveFrom).name)
|
|
pathTo = Path("ComfyUI/models/checkpoints", state["mode"], moveTo, pathFrom.name)
|
|
Path(pathFrom).rename(pathTo)
|
|
state["checkpointFolder"] = moveTo
|
|
return web.Response(status=200)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/move_presetfile")
|
|
async def move_presetfile(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, moveTo = await request.json()
|
|
state.update(stt)
|
|
pathFrom = Path("preset", state["presetFolder"], state["presetFile"])
|
|
pathTo = Path("preset", moveTo, pathFrom.name)
|
|
Path(pathFrom).rename(pathTo)
|
|
state["presetFolder"] = moveTo
|
|
return web.Response(status=200)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/move_lorafile")
|
|
async def move_lorafile(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, moveTo = await request.json()
|
|
state.update(stt)
|
|
pathFrom = Path("ComfyUI/models/loras", state["mode"], state["loraFolder"], state["loraFile"])
|
|
pathTo = Path("ComfyUI/models/loras", state["mode"], moveTo, pathFrom.name)
|
|
Path(pathFrom).rename(pathTo)
|
|
state["loraFolder"] = moveTo
|
|
return web.Response(status=200)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/load_preset")
|
|
async def load_preset(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, loraPromptOnly = await request.json()
|
|
state.update(stt)
|
|
data = param
|
|
with open(Path("preset", state["presetFolder"], state["presetFile"]), "r") as file:
|
|
buf = json.load(file)
|
|
if loraPromptOnly:
|
|
if "lora" in buf:
|
|
data["lora"] = base64.b64decode(buf["lora"]).decode("utf-8")
|
|
else:
|
|
data = buf
|
|
data["prompt"] = base64.b64decode(buf["prompt"]).decode("utf-8")
|
|
data["negativePrompt"] = base64.b64decode(buf["negativePrompt"]).decode("utf-8") if "negativePrompt" in buf else ""
|
|
data["lora"] = base64.b64decode(buf["lora"]).decode("utf-8")
|
|
|
|
param.update(data)
|
|
state["presetTitle"] = Path(state["presetFile"]).stem
|
|
return web.json_response({"lora": data["lora"], "presetTitle": state["presetTitle"]})
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/save_preset")
|
|
async def save_preset(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
[stt, data] = await request.json()
|
|
state.update(stt)
|
|
param.update(data)
|
|
data["prompt"] = base64.b64encode(bytes(data["prompt"], "utf-8")).decode("utf-8")
|
|
data["negativePrompt"] = base64.b64encode(bytes(data["negativePrompt"], "utf-8")).decode("utf-8")
|
|
data["lora"] = base64.b64encode(bytes(data["lora"], "utf-8")).decode("utf-8")
|
|
with open(Path("preset", state["presetFolder"], state["presetTitle"] + ".json"), "w") as file:
|
|
json.dump(data, file)
|
|
return web.Response(status=200)
|
|
|
|
|
|
class FlipStreamLoader:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"default_ckpt": (checkpoints_list,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
|
|
FUNCTION = "loader"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, default_ckpt):
|
|
return hash(param["checkpoint"])
|
|
|
|
def loader(self, default_ckpt):
|
|
global frame_updating
|
|
frame_updating = True
|
|
ckpt_path = None
|
|
if param["checkpoint"]:
|
|
ckpt_path = folder_paths.get_full_path("checkpoints", param["checkpoint"])
|
|
if ckpt_path is None:
|
|
ckpt_path = folder_paths.get_full_path("checkpoints", default_ckpt)
|
|
out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
|
|
return out[:3]
|
|
|
|
|
|
class FlipStreamSource:
|
|
def __init__(self):
|
|
self.device = comfy.model_management.intermediate_device()
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"vae": ("VAE",),
|
|
"width": ("INT", {"default": 512}),
|
|
"height": ("INT", {"default": 512}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE","LATENT","BOOLEAN","INT","INT","INT","FLOAT")
|
|
RETURN_NAMES = ("image","latent","bypass","width","height","frames","videostr")
|
|
OUTPUT_IS_LIST = (True,True,False,False,False,False,False)
|
|
|
|
FUNCTION = "source"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, vae, width, height):
|
|
state["width"] = width
|
|
state["height"] = height
|
|
return hash((width, height, param["frames"], param["videosrc"], param["videofst"], param["videoskp"], param["videostr"]))
|
|
|
|
def source(self, vae, width, height):
|
|
videopath = str(Path("videosrc", param["videosrc"])) if param["videosrc"] else ""
|
|
frames = param["frames"]
|
|
image = None
|
|
latent = None
|
|
bypass = False
|
|
if videopath:
|
|
buf, _ = load_video(videopath, width, height, param["videofst"], param["videoskp"], frames)
|
|
if buf:
|
|
buf = [np.array(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB), dtype=np.float32) / 255 for frame in buf]
|
|
image = torch.zeros([frames, height, width, 3])
|
|
image2 = torch.from_numpy(np.fromiter(buf, np.dtype((np.float32, buf[0].shape))))
|
|
x2 = image.shape[2] // 2
|
|
w2 = image2.shape[2] // 2
|
|
image[:, :, x2 - w2: x2 + w2] = image2
|
|
latent = vae.encode(image)
|
|
bypass = True
|
|
if image is None:
|
|
image = torch.zeros([frames, height, width, 3])
|
|
latent = torch.zeros([frames, 4, height // 8, width // 8], device=self.device)
|
|
frames, h, w, _ = image.shape
|
|
images = [image[i:i + 1, ...] for i in range(image.shape[0])]
|
|
latents = [{"samples": latent[i:i + 1, ...]} for i in range(latent.shape[0])]
|
|
return (images, latents, bypass, w, h, frames, param["videostr"])
|
|
|
|
|
|
class FlipStreamUpdate:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("STRING","STRING","STRING","STRING")
|
|
RETURN_NAMES = ("prompt","batchPrompt","appPrompt","negativePrompt")
|
|
FUNCTION = "update"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls):
|
|
return hash((param["prompt"], param["negativePrompt"], param["lora"]))
|
|
|
|
def update(self):
|
|
global frame_updating
|
|
frame_updating = True
|
|
buf = param["prompt"].split("----")
|
|
prompt = buf[0].replace("{lora}", param["lora"]).strip()
|
|
batchPrompt = buf[1].strip() if len(buf) > 1 else "-\n-\n"
|
|
appPrompt = buf[2].replace("{lora}", param["lora"]).strip() if len(buf) > 2 else ""
|
|
batchPrompt = ",\n".join([f'"{n}":"{item.lstrip("-").strip()}"' for n, item in enumerate(batchPrompt.split("\n"))])
|
|
return (prompt, batchPrompt, appPrompt, param["negativePrompt"])
|
|
|
|
|
|
class FlipStreamPrompt:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"prompt": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
|
"batchPrompt": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
|
"appPrompt": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
|
"frames": ("INT", {"default": 0}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("framePrompt",)
|
|
OUTPUT_IS_LIST = (True,)
|
|
|
|
FUNCTION = "encode"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def encode(self, prompt, batchPrompt, appPrompt, frames):
|
|
framePrompt = []
|
|
item = prompt
|
|
s = json.loads("{" + batchPrompt + "}")
|
|
for i in range(frames):
|
|
if i < len(s):
|
|
item = " ".join([prompt, s[str(i)], appPrompt]).strip()
|
|
framePrompt.append(item)
|
|
return (framePrompt,)
|
|
|
|
|
|
class FlipStreamOption:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"mode": ("STRING", {"default": ""}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("INT","INT","FLOAT","INT",comfy.samplers.KSampler.SAMPLERS,comfy.samplers.KSampler.SCHEDULERS)
|
|
RETURN_NAMES = ("seed","steps","cfg","startstep","sampler","scheduler")
|
|
FUNCTION = "option"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, mode):
|
|
state["mode"] = mode
|
|
return hash((mode, param["seed"], param["steps"], param["cfg"], param["startstep"], param["sampler"]))
|
|
|
|
def option(self, mode):
|
|
global frame_updating
|
|
frame_updating = True
|
|
sampler, scheduler = param["sampler"].split(",")
|
|
return (param["seed"], param["steps"], param["cfg"], param["startstep"], sampler, scheduler)
|
|
|
|
|
|
class FlipStreamSwitchVFI:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"tensor": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
INPUT_IS_LIST = True
|
|
RETURN_TYPES = ("IMAGE","BOOLEAN")
|
|
RETURN_NAMES = ("tensor","bypass")
|
|
FUNCTION = "control"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def control(self, tensor):
|
|
tensor = torch.cat(tensor, dim=0)
|
|
return (tensor, tensor.shape[0] >= 2)
|
|
|
|
|
|
class FlipStreamViewer:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"tensor": ("IMAGE",),
|
|
"allowip": ("STRING", {"default": ""}),
|
|
"wd14exc": ("STRING", {"default": ""}),
|
|
"idle": ("FLOAT", {"default": 1.0}),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, tensor, allowip, wd14exc, idle):
|
|
global allowed_ips
|
|
global exclude_tags
|
|
allowed_ips = ["127.0.0.1"] + list(map(str.strip, allowip.split(",")))
|
|
exclude_tags = wd14exc
|
|
time.sleep(idle)
|
|
return None
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "update_frame"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def update_frame(self, tensor, allowip, wd14exc, idle):
|
|
global frame_updating
|
|
global frame_buffer
|
|
buffer = []
|
|
for i in range(tensor.shape[0]):
|
|
image = Image.fromarray(np.clip(255. * tensor[i,:,:,:].cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
|
with BytesIO() as image_bytes:
|
|
image.save(image_bytes, format="WEBP", quality=100)
|
|
buffer.append(image_bytes.getvalue())
|
|
buffer += buffer[::-1]
|
|
frame_updating = False
|
|
frame_buffer = buffer
|
|
return ()
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"FlipStreamLoader": FlipStreamLoader,
|
|
"FlipStreamSource": FlipStreamSource,
|
|
"FlipStreamUpdate": FlipStreamUpdate,
|
|
"FlipStreamPrompt": FlipStreamPrompt,
|
|
"FlipStreamOption": FlipStreamOption,
|
|
"FlipStreamSwitchVFI": FlipStreamSwitchVFI,
|
|
"FlipStreamViewer": FlipStreamViewer,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"FlipStreamLoader": "FlipStreamLoader",
|
|
"FlipStreamSource": "FlipStreamSource",
|
|
"FlipStreamUpdate": "FlipStreamUpdate",
|
|
"FlipStreamPrompt": "FlipStreamPrompt",
|
|
"FlipStreamOption": "FlipStreamOption",
|
|
"FlipStreamSwitchVFI": "FlipStreamSwitchVFI",
|
|
"FlipStreamViewer": "FlipStreamViewer",
|
|
}
|