- Changed FlipStreamChatJson: `temperature` and `top_p` are now inputs. - Add FlipStreamViewerVideo: streams frames and optional audio to the viewer. - Add FlipStreamFileSelect_DiffusionModels. - Add FlipStreamLogger. - Some fixes.
4211 lines
150 KiB
Python
4211 lines
150 KiB
Python
import asyncio
|
|
import base64
|
|
import hashlib
|
|
import html
|
|
import io
|
|
import itertools
|
|
import json
|
|
import threading
|
|
import time
|
|
import subprocess
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import imageio.v3 as iio
|
|
import requests
|
|
import torch
|
|
import torch.nn.functional as NNF
|
|
import transformers
|
|
import numpy as np
|
|
import mss
|
|
from PIL import Image, ImageDraw
|
|
from aiohttp import web
|
|
from aiohttp.web_exceptions import HTTPForbidden, HTTPError
|
|
|
|
import server
|
|
import folder_paths
|
|
import comfy
|
|
from nodes import CheckpointLoaderSimple, VAELoader
|
|
|
|
import sys
|
|
sys.path.append("ComfyUI/custom_nodes")
|
|
|
|
try:
|
|
from llama_cpp import Llama # llama-cpp-python
|
|
except:
|
|
Llama = None
|
|
|
|
try:
|
|
from llama_cpp import LlamaRAMCache
|
|
except:
|
|
LlamaRAMCache = None
|
|
|
|
try:
|
|
rembg = __import__("comfyui-inspyrenet-rembg")
|
|
except:
|
|
rembg = None
|
|
|
|
try:
|
|
florence2 = __import__("comfyui-florence2")
|
|
except:
|
|
florence2 = None
|
|
|
|
try:
|
|
film = __import__("comfyui-frame-interpolation.vfi_models.film").vfi_models.film
|
|
except:
|
|
film = None
|
|
|
|
try:
|
|
import av
|
|
except:
|
|
av = None
|
|
|
|
try:
|
|
from comfyui_tensorrt import TensorRTLoader
|
|
except:
|
|
TensorRTLoader = None
|
|
|
|
def btoa_utf8(value):
|
|
return base64.b64encode(value.encode("utf-8")).decode()
|
|
|
|
def atob_utf8(value):
|
|
if value is None:
|
|
return None
|
|
if value == "":
|
|
return ""
|
|
return base64.b64decode(value.encode()).decode("utf-8")
|
|
|
|
STREAM_COMPRESSION = 1
|
|
|
|
|
|
def encode_video_mp4(buf, fps, crf, preset, audio=None):
|
|
if av is None:
|
|
raise RuntimeError("FlipStreamViewerVideo: PyAV required. (pip install av)")
|
|
h, w = buf.shape[1] - buf.shape[1] % 2, buf.shape[2] - buf.shape[2] % 2
|
|
out = io.BytesIO()
|
|
container = av.open(out, "w", format="mp4", options={"movflags": "frag_keyframe+empty_moov+default_base_moof"})
|
|
try:
|
|
vs = container.add_stream("libx264", rate=int(fps))
|
|
vs.width, vs.height = w, h
|
|
vs.pix_fmt = "yuv420p"
|
|
vs.options = {"crf": str(crf), "preset": preset}
|
|
if audio is not None:
|
|
wav = np.ascontiguousarray(audio["waveform"][0].cpu().numpy().astype(np.float32)[:2])
|
|
layout = "mono" if wav.shape[0] == 1 else "stereo"
|
|
astream = container.add_stream("aac", rate=int(audio["sample_rate"]), layout=layout)
|
|
aframe = av.AudioFrame.from_ndarray(wav, format="fltp", layout=layout)
|
|
aframe.sample_rate = int(audio["sample_rate"])
|
|
aframe.pts = 0
|
|
for image in buf:
|
|
frame = av.VideoFrame.from_ndarray(np.ascontiguousarray(image[:h, :w, :3]), format="rgb24")
|
|
for packet in vs.encode(frame):
|
|
container.mux(packet)
|
|
for packet in vs.encode(None):
|
|
container.mux(packet)
|
|
if audio is not None:
|
|
for packet in astream.encode(aframe):
|
|
container.mux(packet)
|
|
for packet in astream.encode(None):
|
|
container.mux(packet)
|
|
finally:
|
|
container.close()
|
|
return out.getvalue()
|
|
|
|
|
|
UPDATE_DELAY = 1.0
|
|
allowed_ips = ["127.0.0.1"]
|
|
refresh_updating = 0
|
|
refresh_data = {}
|
|
refresh_param = {}
|
|
default_param = {"lora": "", "_capture_offsetX": 0, "_capture_offsetY": 0, "_capture_scale": 100}
|
|
param = default_param.copy()
|
|
default_state = {"presetTitle": time.strftime("%Y%m%d-%H%M"), "presetFolder": "", "presetFile": "", "loraRate": "1", "loraRank": "0", "loraMode": "", "loraFolder": "", "loraFile": "", "loraTagOptions": "[]", "loraTag": "", "loraLinkHref": "", "loraPreviewSrc": "", "darker": 0.0, "lastElapsed": 0}
|
|
state = default_state.copy()
|
|
frame_buffer = []
|
|
frame_mtime = 0
|
|
frame_fps = 16
|
|
stream_video = b""
|
|
stream_video_mtime = 0
|
|
stream_video_audio = False
|
|
setframe_mtime = 0
|
|
setframe_buffer = []
|
|
|
|
class AnyType(str):
|
|
def __ne__(self, __value: object) -> bool:
|
|
return False
|
|
|
|
anytype = AnyType("*")
|
|
|
|
|
|
HEAD=r"""
|
|
<head>
|
|
<style type="text/css">
|
|
|
|
html {
|
|
background-color: black;
|
|
}
|
|
|
|
body {
|
|
color: lightgray;
|
|
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 {
|
|
padding: 2px;
|
|
min-width: 2em;
|
|
}
|
|
|
|
hr {
|
|
background-color: dimgray;
|
|
border: none;
|
|
height: 3px;
|
|
padding: 0px;
|
|
margin: 1px;
|
|
}
|
|
|
|
button.disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
select {
|
|
color: lightgray;
|
|
background-color: black;
|
|
font-size: 100%;
|
|
}
|
|
|
|
div.row {
|
|
display: flex;
|
|
position: relative;
|
|
}
|
|
|
|
div#presetXorkeyInputDiv {
|
|
display: none;
|
|
position: relative;
|
|
}
|
|
|
|
.FlipStreamSlider {
|
|
width: 50%;
|
|
}
|
|
|
|
.FlipStreamInputBox {
|
|
width: 100%;
|
|
}
|
|
|
|
.FlipStreamSelectBox,
|
|
.FlipStreamFolderSelect,
|
|
.FlipStreamSizeSelect,
|
|
.FlipStreamFileSelect,
|
|
.FlipStreamMoveFileSelect {
|
|
width: 100%;
|
|
}
|
|
|
|
.FlipStreamMoveFileSelect {
|
|
display: none;
|
|
}
|
|
|
|
.FlipStreamPasteBox {
|
|
border: 1px dimgray;
|
|
max-width: 100%;
|
|
min-height: 32px;
|
|
max-height: 160px;
|
|
object-fit: scale-down;
|
|
}
|
|
|
|
.FlipStreamPreviewBox {
|
|
border: 1px dimgray;
|
|
max-width: 100%;
|
|
min-height: 32px;
|
|
max-height: 160px;
|
|
object-fit: scale-down;
|
|
}
|
|
|
|
.FlipStreamPreviewRoi {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
#leftPanel,
|
|
#rightPanel,
|
|
#captureLeftPanel,
|
|
#captureRightPanel,
|
|
#tagLeftPanel,
|
|
#tagRightPanel,
|
|
#presetLeftPanel,
|
|
#presetRightPanel {
|
|
width: 12%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
#centerPanel,
|
|
#captureCenterPanel,
|
|
#tagCenterPanel,
|
|
#presetCenterPanel {
|
|
width: 76%;
|
|
text-align: center;
|
|
}
|
|
|
|
#captureCenterPanel,
|
|
#tagCenterPanel {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
#mainDialog, #captureDialog, #tagDialog, #presetDialog {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
#mainDialog {
|
|
display: flex;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-blend-mode: overlay;
|
|
}
|
|
|
|
#streamVideo {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: none;
|
|
z-index: -1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#captureDialog, #tagDialog, #presetDialog {
|
|
display: none;
|
|
z-index: 999;
|
|
}
|
|
|
|
#presetFolderSelect,
|
|
#movePresetSelect,
|
|
#loraModeSelect,
|
|
#loraFolderSelect,
|
|
#moveLoraSelect {
|
|
width: 100%;
|
|
}
|
|
|
|
#movePresetSelect,
|
|
#moveLoraSelect {
|
|
display: none;
|
|
}
|
|
|
|
#presetXorkey {
|
|
display: none;
|
|
}
|
|
|
|
#presetFileSelect,
|
|
#presetTitleInput,
|
|
#loraFileSelect,
|
|
#loraTagSelect {
|
|
width: 80%;
|
|
}
|
|
|
|
#darkerRange {
|
|
width: 50%;
|
|
}
|
|
|
|
#messageBox {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
padding: 10px;
|
|
background: transparent;
|
|
text-align: left;
|
|
user-select: none;
|
|
font-size: 1rem;
|
|
text-shadow:
|
|
black 2px 0px, black -2px 0px,
|
|
black 0px -2px, black 0px 2px,
|
|
black 2px 2px , black -2px 2px,
|
|
black 2px -2px, black -2px -2px,
|
|
black 1px 2px, black -1px 2px,
|
|
black 1px -2px, black -1px -2px,
|
|
black 2px 1px, black -2px 1px,
|
|
black 2px -1px, black -2px -1px;
|
|
}
|
|
|
|
#loraLink img {
|
|
max-width: 100%;
|
|
max-height: 160px;
|
|
width: auto;
|
|
height: auto;
|
|
margin: auto;
|
|
display: block;
|
|
}
|
|
|
|
#offsetXRange,
|
|
#offsetYRange,
|
|
#scaleRange {
|
|
width: 80%;
|
|
}
|
|
|
|
</style>
|
|
<title>FlipStreamViewer</title>
|
|
</head>
|
|
"""
|
|
|
|
SCRIPT_PARAM=r"""
|
|
function btoa_utf8(str) {
|
|
return btoa(unescape(encodeURIComponent(str)));
|
|
}
|
|
|
|
function atob_utf8(str) {
|
|
if (!str) {
|
|
return "";
|
|
}
|
|
return decodeURIComponent(escape(atob(str)));
|
|
}
|
|
|
|
function getStateAsJson(force_state={}) {
|
|
const presetTitle = document.getElementById("presetTitleInput").value;
|
|
const presetFolder = document.getElementById("presetFolderSelect").value;
|
|
const presetFile = document.getElementById("presetFileSelect").value;
|
|
const loraMode = document.getElementById("loraModeSelect").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 darker = parseFloat(document.getElementById("darkerRange").value);
|
|
const lastElapsed = document.getElementById("statusLastElapsed").value;
|
|
var res = { presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, loraRate: loraRate, loraRank: loraRank, loraMode: loraMode, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, darker: darker, lastElapsed: lastElapsed };
|
|
document.querySelectorAll('.FlipStreamFolderSelect').forEach(x => res[x.name] = x.value);
|
|
res = Object.assign(res, force_state);
|
|
return res;
|
|
}
|
|
|
|
function getParamAsJson(force_param={}) {
|
|
const lora = btoa_utf8(document.getElementById("loraInput").value.trim());
|
|
const _capture_offsetX = parseInt(document.getElementById("offsetXRange").value);
|
|
const _capture_offsetY = parseInt(document.getElementById("offsetYRange").value);
|
|
const _capture_scale = parseInt(document.getElementById("scaleRange").value);
|
|
|
|
var res = { lora: lora, _capture_offsetX: _capture_offsetX, _capture_offsetY: _capture_offsetY, _capture_scale: _capture_scale }
|
|
document.querySelectorAll('.FlipStreamSlider').forEach(x => res[x.name] = x.value);
|
|
document.querySelectorAll('.FlipStreamTextBox').forEach(x => res[x.name] = btoa_utf8(x.value));
|
|
document.querySelectorAll('.FlipStreamInputBox').forEach(x => res[x.name] = x.value);
|
|
document.querySelectorAll('.FlipStreamSelectBox').forEach(x => res[x.name] = x.value);
|
|
document.querySelectorAll('.FlipStreamSizeSelect').forEach(x => res[x.name] = x.value);
|
|
document.querySelectorAll('.FlipStreamFileSelect').forEach(x => res[x.name] = x.value);
|
|
res = Object.assign(res, force_param);
|
|
return res;
|
|
}
|
|
|
|
function setParam(param) {
|
|
document.getElementById("loraInput").value = param.lora || "";
|
|
document.getElementById("offsetXRange").value = param.offsetX || 0;
|
|
document.getElementById("offsetYRange").value = param.offsetY || 0;
|
|
document.getElementById("scaleRange").value = param.scale || 100;
|
|
for (let key in param) {
|
|
const elem = document.querySelector(`input[name=${key}]`);
|
|
elem.value = param[key] || elem.value;
|
|
}
|
|
}
|
|
|
|
function updateParam(reload=false, run_once=false, force_state={}, force_param={}, search="") {
|
|
fetch("/flipstreamviewer/update_param", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(force_state), getParamAsJson(force_param)]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
if (run_once) {
|
|
fetch('/flipstreamviewer/run_once');
|
|
}
|
|
if (reload) {
|
|
reloadPage(search);
|
|
}
|
|
} else {
|
|
alert("Failed to update parameter.");
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while updating parameter.");
|
|
});
|
|
}
|
|
|
|
function setupPreviewRoi() {
|
|
document.querySelectorAll('.FlipStreamPreviewRoi').forEach(canvas => {
|
|
const ctx = canvas.getContext('2d');
|
|
const img = canvas.parentElement.querySelector('img');
|
|
const label = canvas.id.replace("PreviewRoi", "");
|
|
img.onload = () => {
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
};
|
|
|
|
let drawing = false, x, y;
|
|
canvas.onmousedown = e => { drawing = true; [x, y] = [e.offsetX, e.offsetY]; };
|
|
canvas.onmousemove = e => { if (drawing) { canvas.width = canvas.width; ctx.strokeStyle = 'red'; ctx.strokeRect(x, y, e.offsetX - x, e.offsetY - y); } };
|
|
canvas.onmouseup = e => {
|
|
if (!drawing) return;
|
|
drawing = false;
|
|
var sx = x / canvas.width;
|
|
var sy = y / canvas.height;
|
|
var ex = e.offsetX / canvas.width;
|
|
var ey = e.offsetY / canvas.height;
|
|
if (ex <= sx || ey <= sy) {
|
|
sx = 0;
|
|
sy = 0;
|
|
ex = 1;
|
|
ey = 1;
|
|
}
|
|
fetch('/flipstreamviewer/preview_setroi', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
label: label,
|
|
sx: sx,
|
|
sy: sy,
|
|
ex: ex,
|
|
ey: ey
|
|
})
|
|
});
|
|
};
|
|
});
|
|
}
|
|
setupPreviewRoi();
|
|
"""
|
|
|
|
SCRIPT_PRESET=r"""
|
|
function loadPreset(loraPromptOnly=false, force_state={}, search="") {
|
|
const xorkey = document.getElementById("presetXorkeyInput").value;
|
|
fetch("/flipstreamviewer/load_preset", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(force_state), loraPromptOnly, xorkey]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => response.json()).then(json => {
|
|
if (loraPromptOnly) {
|
|
document.getElementById("loraInput").value = atob_utf8(json.lora);
|
|
document.getElementById("presetTitleInput").value = json.presetTitle;
|
|
}
|
|
else {
|
|
fetch('/flipstreamviewer/run_once');
|
|
reloadPage(search);
|
|
}
|
|
}).catch(error => {
|
|
alert("An error occurred while loading preset: " + error);
|
|
});
|
|
}
|
|
|
|
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("/flipstreamviewer/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;
|
|
const xorkey = document.getElementById("presetXorkeyInput").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("/flipstreamviewer/save_preset", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), getParamAsJson(), xorkey]),
|
|
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.");
|
|
});
|
|
}
|
|
|
|
function showPresetDialog() {
|
|
document.getElementById("mainDialog").style.display = "none";
|
|
document.getElementById("presetDialog").style.display = "flex";
|
|
}
|
|
|
|
function closePresetDialog() {
|
|
document.getElementById("mainDialog").style.display = "flex";
|
|
document.getElementById("presetDialog").style.display = "none";
|
|
}
|
|
"""
|
|
|
|
SCRIPT_CKPT=r"""
|
|
function showMoveFileSelect(obj) {
|
|
obj.style.display="block";
|
|
obj.value="";
|
|
}
|
|
|
|
function moveFile(folder_path, mode, label) {
|
|
const moveFrom = document.getElementById(label + "FileSelect").value;
|
|
const moveTo = document.getElementById(label + "MoveFileSelect").value;
|
|
if (!moveFrom || !moveTo) {
|
|
return;
|
|
}
|
|
fetch("/flipstreamviewer/move_file", {
|
|
method: "POST",
|
|
body: JSON.stringify([getStateAsJson(), folder_path, mode, label, moveFrom, moveTo]),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => {
|
|
if (response.ok) {
|
|
alert("Moved successfully!");
|
|
} else {
|
|
alert("Failed to move.");
|
|
}
|
|
reloadPage();
|
|
}).catch(error => {
|
|
alert("An error occurred while moving.");
|
|
});
|
|
}
|
|
"""
|
|
|
|
SCRIPT_LORA=r"""
|
|
function selectLoraFile() {
|
|
const loraFileSelect = document.getElementById("loraFileSelect");
|
|
const loraFile = loraFileSelect.value;
|
|
const loraFolder = document.getElementById("loraFolderSelect").value;
|
|
|
|
if (loraFile) toggleLora();
|
|
|
|
fetch("/flipstreamviewer/get_lorainfo", {
|
|
method: "POST",
|
|
body: JSON.stringify({loraFolder: loraFolder, loraFile: loraFile}),
|
|
headers: {"Content-Type": "application/json"}
|
|
}).then(response => response.json()).then(json => {
|
|
const tagSelect = document.getElementById("loraTagSelect");
|
|
tagSelect.innerHTML = '<option value="">tags</option>';
|
|
if (json.ss_tag_frequency) {
|
|
const tags = {}, datasets = JSON.parse(json.ss_tag_frequency);
|
|
for (const set of Object.values(datasets)) {
|
|
for (const [t, v] of Object.entries(set)) tags[t] = (tags[t] || 0) + v;
|
|
}
|
|
Object.entries(tags).sort((a, b) => b[1] - a[1]).forEach(([t, v]) => {
|
|
tagSelect.add(new Option(`${t}:${v}`, t));
|
|
});
|
|
}
|
|
document.getElementById("loraLink").href = json._lorapreview_href || "";
|
|
document.getElementById("loraPreview").src = json._lorapreview_src || "";
|
|
});
|
|
}
|
|
|
|
function toggleLora() {
|
|
const loraMode = document.getElementById("loraModeSelect").value;
|
|
const loraFileSelect = document.getElementById("loraFileSelect");
|
|
const loraName = loraFileSelect.options[loraFileSelect.selectedIndex].text;
|
|
const loraRate = document.getElementById("loraRate").value;
|
|
const loraInput = document.getElementById("loraInput");
|
|
|
|
const re = new RegExp("[\\n]?<lora:" + loraName + ":[-]?[0-9.]+>", "g");
|
|
const match = re.exec(loraInput.value);
|
|
|
|
if (match) {
|
|
loraInput.setRangeText("", match.index, match.index + match[0].length, 'preserve');
|
|
} else {
|
|
const loraTag = `<lora:${loraName}:${loraRate}>`;
|
|
const container = `@${loraMode}{`;
|
|
const containerIdx = loraMode ? loraInput.value.indexOf(container) : -1;
|
|
|
|
if (containerIdx !== -1) {
|
|
const pos = containerIdx + container.length;
|
|
loraInput.setRangeText("\n" + loraTag, pos, pos, 'end');
|
|
} else {
|
|
const prefix = loraInput.value.trim() ? "\n" : "";
|
|
const text = loraMode ? `${prefix}@${loraMode}{\n${loraTag}\n}` : `${prefix}${loraTag}`;
|
|
loraInput.setRangeText(text, loraInput.value.length, loraInput.value.length, 'end');
|
|
if (loraMode) loraInput.setSelectionRange(loraInput.value.length - 2, loraInput.value.length - 2);
|
|
}
|
|
}
|
|
loraInput.focus();
|
|
}
|
|
|
|
|
|
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("/flipstreamviewer/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_TAG=r"""
|
|
function toggleTag() {
|
|
const el = document.getElementById("loraInput"), s = document.getElementById("loraTagSelect");
|
|
const tag = s.value || Array.from(s.options).slice(1, parseInt(document.getElementById("loraRank").value) + 1).map(o => o.value).join(", ");
|
|
const re = new RegExp(`(\\s*,\\s*)?${tag}(\\s*,\\s*)?`, "g");
|
|
|
|
if (el.value.match(re)) {
|
|
const match = re.exec(el.value);
|
|
el.focus();
|
|
el.setRangeText("", match.index, match.index + match[0].length, "end");
|
|
} else {
|
|
const pre = (el.selectionStart > 0 && !/[,\s\n]$/.test(el.value.slice(0, el.selectionStart))) ? ", " : "";
|
|
el.focus();
|
|
el.setRangeText(pre + tag, el.selectionStart, el.selectionEnd, 'end');
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
const count = tagCenterPanel.children.length;
|
|
|
|
for (const child of Array.from(tagCenterPanel.children)) {
|
|
if (!child.classList.contains("disabled")) {
|
|
child.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;
|
|
}
|
|
|
|
if (tags[i] == "----") {
|
|
if (count == 0) {
|
|
const hr = document.createElement("hr");
|
|
hr.classList.add("tags");
|
|
hr.classList.add("separator");
|
|
tagCenterPanel.appendChild(hr);
|
|
}
|
|
} else {
|
|
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 getRandomInt(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
}
|
|
|
|
function tagCSel() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
Array.from(tagCenterPanel.children).forEach(child => child.classList.contains("disabled") || child.classList.add("disabled"));
|
|
}
|
|
|
|
function tagRSel() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
var group = [];
|
|
Array.from(tagCenterPanel.children).forEach(child => {
|
|
if (child.classList.contains("separator") && group.length > 0) {
|
|
const r = getRandomInt(0, group.length);
|
|
if (r != group.length) {
|
|
group[r].classList.remove("disabled");
|
|
}
|
|
group = [];
|
|
} else {
|
|
group.push(child);
|
|
}
|
|
});
|
|
const r = getRandomInt(0, group.length);
|
|
if (r != group.length) {
|
|
group[r].classList.remove("disabled");
|
|
}
|
|
}
|
|
|
|
function tagOK() {
|
|
const tagCenterPanel = document.getElementById("tagCenterPanel");
|
|
var selectedTags = [];
|
|
Array.from(tagCenterPanel.children).forEach(child => {
|
|
if (!child.classList.contains("separator") && !child.classList.contains("disabled")) {
|
|
selectedTags.push(child.value);
|
|
}
|
|
});
|
|
loraInput.value = selectedTags.join(", ");
|
|
closeTagDialog();
|
|
}
|
|
|
|
function closeTagDialog() {
|
|
document.getElementById("tagDialog").style.display = "none";
|
|
}
|
|
"""
|
|
|
|
SCRIPT_VIEW=r"""
|
|
var toggleViewFlag = true;
|
|
var streamViewFlag = true;
|
|
var streamInfo = { mtime: 0, fps: 8, count: 0 };
|
|
var streamMTime = 0;
|
|
var streamCache = [];
|
|
var streamIndex = 0;
|
|
var streamVideo = null;
|
|
var streamMuted = localStorage.getItem('streamMuted') !== 'false';
|
|
var streamVolume = parseFloat(localStorage.getItem('streamVolume') ?? '0') / 100;
|
|
|
|
function detailsState() {
|
|
const ds = document.querySelectorAll('details');
|
|
ds.forEach((d, i) => {
|
|
const s = sessionStorage.getItem(`details_state-${i}`);
|
|
if (s) d.open = s !== 'closed';
|
|
});
|
|
document.addEventListener('toggle', e => {
|
|
sessionStorage.setItem(`details_state-${Array.from(ds).indexOf(e.target)}`, e.target.open ? 'open' : 'closed');
|
|
}, true);
|
|
}
|
|
detailsState();
|
|
|
|
function onPasteBox() {
|
|
document.querySelectorAll('.FlipStreamPasteBox').forEach(i => {
|
|
i.tabIndex = 0;
|
|
i.onclick = () => i.focus();
|
|
i.onpaste = e => {
|
|
const f = e.clipboardData.files[0];
|
|
if (f?.type[0] == 'i') fetch(`/flipstreamviewer/paste_upload?label=${i.name}`, {method:'POST', body:f});
|
|
};
|
|
i.onkeydown = e => {
|
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
fetch(`/flipstreamviewer/paste_remove?label=${i.name}`, {method:'POST'});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
onPasteBox();
|
|
|
|
function toggleView() {
|
|
const leftPanel = document.getElementById("leftPanel");
|
|
const rightPanel = document.getElementById("rightPanel");
|
|
const presetLeftPanel = document.getElementById("presetLeftPanel");
|
|
const presetRightPanel = document.getElementById("presetRightPanel");
|
|
const messageBox = document.getElementById("messageBox");
|
|
if (toggleViewFlag) {
|
|
leftPanel.style.visibility = "hidden";
|
|
rightPanel.style.visibility = "hidden";
|
|
presetLeftPanel.style.visibility = "hidden";
|
|
presetRightPanel.style.visibility = "hidden";
|
|
messageBox.style.visibility = "hidden";
|
|
toggleViewFlag = false;
|
|
} else {
|
|
leftPanel.style.visibility = "visible";
|
|
rightPanel.style.visibility = "visible";
|
|
presetLeftPanel.style.visibility = "visible";
|
|
presetRightPanel.style.visibility = "visible";
|
|
messageBox.style.visibility = "visible";
|
|
toggleViewFlag = true;
|
|
}
|
|
}
|
|
|
|
function hideView() {
|
|
const leftPanel = document.getElementById("leftPanel");
|
|
const centerPanel = document.getElementById("centerPanel");
|
|
const rightPanel = document.getElementById("rightPanel");
|
|
const presetLeftPanel = document.getElementById("presetLeftPanel");
|
|
const presetRightPanel = document.getElementById("presetRightPanel");
|
|
const messageBox = document.getElementById("messageBox");
|
|
closeCaptureDialog();
|
|
messageBox.style.visibility = "hidden";
|
|
document.getElementById("loraPreview").src = "";
|
|
document.querySelectorAll('.FlipStreamPreviewBox').forEach(x => x.src = "");
|
|
document.querySelectorAll('.FlipStreamPasteBox').forEach(x => x.src = "");
|
|
leftPanel.style.visibility = "hidden";
|
|
centerPanel.style.visibility = "hidden";
|
|
rightPanel.style.visibility = "hidden";
|
|
presetLeftPanel.style.visibility = "hidden";
|
|
presetRightPanel.style.visibility = "hidden";
|
|
toggleViewFlag = false;
|
|
streamViewFlag = false;
|
|
}
|
|
setTimeout(hideView, 300 * 1000);
|
|
|
|
async function updateStreamView() {
|
|
const container = document.getElementById('mainDialog');
|
|
if (streamVideo) {
|
|
streamVideo.style.display = streamViewFlag ? '' : 'none';
|
|
if (!streamViewFlag) {
|
|
streamVideo.pause();
|
|
} else if (streamVideo.paused) {
|
|
// autoplay with sound needs a user gesture
|
|
streamVideo.play().catch(() => {
|
|
streamVideo.muted = true;
|
|
document.getElementById('muteButton').innerHTML = '🔇';
|
|
streamVideo.play().catch(() => {});
|
|
});
|
|
}
|
|
}
|
|
if (streamViewFlag && streamCache.length > 0) {
|
|
const img = streamCache[streamIndex];
|
|
container.style.backgroundImage = `url(${img.src})`;
|
|
streamIndex = (streamIndex + 1) % streamCache.length;
|
|
} else {
|
|
container.style.backgroundImage = 'none';
|
|
}
|
|
}
|
|
|
|
function setStreamVideo(info) {
|
|
if (!streamVideo) {
|
|
streamVideo = document.createElement('video');
|
|
streamVideo.id = 'streamVideo';
|
|
streamVideo.loop = true;
|
|
streamVideo.playsInline = true;
|
|
document.getElementById('mainDialog').prepend(streamVideo);
|
|
onInputDarker();
|
|
}
|
|
streamCache = [];
|
|
streamVideo.muted = streamMuted;
|
|
streamVideo.volume = streamVolume;
|
|
streamVideo.src = `/flipstreamviewer/stream/video.mp4?mtime=${info.mtime}`;
|
|
document.getElementById('streamAudioRow').style.display = info.audio ? 'contents' : 'none';
|
|
document.getElementById('muteButton').innerHTML = streamMuted ? '🔇' : '🔊';
|
|
document.getElementById('volumeRange').value = streamVolume * 100;
|
|
}
|
|
|
|
function clearStreamVideo() {
|
|
if (streamVideo) {
|
|
streamVideo.remove();
|
|
streamVideo = null;
|
|
}
|
|
document.getElementById('streamAudioRow').style.display = 'none';
|
|
}
|
|
|
|
function toggleMute() {
|
|
streamMuted = streamVideo ? !streamVideo.muted : !streamMuted;
|
|
localStorage.setItem('streamMuted', streamMuted);
|
|
document.getElementById('muteButton').innerHTML = streamMuted ? '🔇' : '🔊';
|
|
if (streamVideo) {
|
|
streamVideo.muted = streamMuted;
|
|
}
|
|
}
|
|
|
|
function setVolume(value) {
|
|
streamVolume = value / 100;
|
|
localStorage.setItem('streamVolume', value);
|
|
if (streamVideo) {
|
|
streamVideo.volume = streamVolume;
|
|
}
|
|
}
|
|
var updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps);
|
|
|
|
async function refreshView() {
|
|
const data = await fetch("/flipstreamviewer/refresh_view")
|
|
.then(r => r.json()).catch(e => ({ status: "Fails to refresh view: " + e }));
|
|
if (data.status_elapsed == 0 && document.getElementById("statusElapsed").value != 0) {
|
|
document.getElementById("statusLastElapsed").value = document.getElementById("statusElapsed").value;
|
|
}
|
|
document.getElementById("statusSummary").textContent = `${new Date().toTimeString().slice(0, 5)} [${data.status_elapsed || 0}s] ${data.status_summary || ""}`;
|
|
document.getElementById("statusElapsed").value = data.status_elapsed || 0;
|
|
document.getElementById("statusInfo").textContent = data.status_info || "Empty";
|
|
document.getElementById("messageBox").innerText = atob_utf8(data.message) || "";
|
|
document.getElementById("messageBox").style.fontSize = data.message_fontsize || "1rem";
|
|
for (const k in data.param) {
|
|
const el = document.querySelector(`[name="${k}"]`);
|
|
if (el) {
|
|
if (el.classList.contains('FlipStreamTextBox')) {
|
|
el.value = atob_utf8(data.param[k]);
|
|
} else if (el.type === 'range') {
|
|
el.value = parseFloat(data.param[k]);
|
|
} else {
|
|
el.value = data.param[k];
|
|
}
|
|
}
|
|
}
|
|
document.querySelectorAll('.FlipStreamLogBox').forEach(async x => {
|
|
if (x.id in data.log) {
|
|
x.value = atob_utf8(data.log[x.id]) || "";
|
|
} else {
|
|
x.value = "";
|
|
}
|
|
});
|
|
if (streamViewFlag) {
|
|
darker = document.getElementById("darkerRange").value;
|
|
document.querySelectorAll('.FlipStreamPreviewBox').forEach(async x => {
|
|
if (x.src != "") {
|
|
x.src = `/flipstreamviewer/preview?label=${x.name}&mtime=${data.preview_mtime[x.id] || 0}`;
|
|
}
|
|
});
|
|
document.querySelectorAll('.FlipStreamPasteBox').forEach(async x => {
|
|
if (x.src != "") {
|
|
x.src = `/flipstreamviewer/paste?label=${x.name}&mtime=${data.mtime[x.name+'_mtime'] || 0}`;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
setInterval(refreshView, 1000);
|
|
|
|
async function applyStreamInfo(info) {
|
|
streamInfo = info;
|
|
streamMTime = info.mtime;
|
|
clearInterval(updateStreamInterval);
|
|
streamIndex = 0;
|
|
if (info.video) {
|
|
setStreamVideo(info);
|
|
} else {
|
|
clearStreamVideo();
|
|
streamCache = await Promise.all(
|
|
Array.from({ length: info.count }, (_, j) =>
|
|
new Promise(resolve => {
|
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
|
img.onerror = () => resolve(img);
|
|
img.src = `/flipstreamviewer/stream/${j}.png?mtime=${info.mtime}`;
|
|
})
|
|
)
|
|
);
|
|
}
|
|
updateStreamInterval = setInterval(updateStreamView, 1000 / info.fps);
|
|
}
|
|
|
|
async function watchStream() {
|
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
while (true) {
|
|
if (!streamViewFlag) {
|
|
await sleep(1000);
|
|
continue;
|
|
}
|
|
try {
|
|
const response = await fetch(`/flipstreamviewer/stream/info?since=${streamMTime}&timeout=25`);
|
|
if (!response.ok) throw new Error(response.status);
|
|
const info = await response.json();
|
|
if (info.mtime != streamMTime) {
|
|
await applyStreamInfo(info);
|
|
}
|
|
} catch (e) {
|
|
await sleep(1000);
|
|
}
|
|
}
|
|
}
|
|
watchStream();
|
|
"""
|
|
|
|
SCRIPT_CAPTURE=r"""
|
|
let capturedCanvas = document.createElement("canvas");
|
|
async function capture() {
|
|
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();
|
|
});
|
|
}
|
|
|
|
function updateCanvas() {
|
|
document.getElementById("captureDialog").style.display = "flex";
|
|
const canvas = document.getElementById("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
const _capture_offsetX = parseInt(document.getElementById("offsetXRange").value, 10);
|
|
const _capture_offsetY = parseInt(document.getElementById("offsetYRange").value, 10);
|
|
const _capture_scale = parseInt(document.getElementById("scaleRange").value, 10) / 100;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.save();
|
|
ctx.scale(_capture_scale, _capture_scale);
|
|
ctx.translate(_capture_offsetX, _capture_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();
|
|
}
|
|
|
|
async function setFrame() {
|
|
const canvas = document.getElementById("canvas");
|
|
await fetch("/flipstreamviewer/set_frame", {
|
|
method: "POST",
|
|
body: canvas.toDataURL("image/png"),
|
|
headers: { "Content-Type": "image/png" }
|
|
});
|
|
closeCaptureDialog();
|
|
}
|
|
"""
|
|
|
|
SCRIPT_QUERY=r"""
|
|
function onInputDarker(value=-1) {
|
|
value = parseFloat(value);
|
|
if (value < 0) {
|
|
value = document.getElementById("darkerRange").value;
|
|
} else {
|
|
document.getElementById("darkerRange").value = value;
|
|
}
|
|
document.getElementById("darkerValue").innerText = value;
|
|
document.getElementById('mainDialog').style.backgroundColor = 'rgba(0,0,0,' + value + ')';
|
|
document.getElementById('loraPreview').style.filter = `brightness(${1 - value})`;
|
|
if (streamVideo) {
|
|
streamVideo.style.filter = `brightness(${1 - value})`;
|
|
}
|
|
document.querySelectorAll('.FlipStreamPreviewBox').forEach(async x => {
|
|
x.style.filter = `brightness(${1 - value})`;
|
|
});
|
|
document.querySelectorAll('.FlipStreamPasteBox').forEach(async x => {
|
|
x.style.filter = `brightness(${1 - value})`;
|
|
});
|
|
}
|
|
onInputDarker();
|
|
|
|
function reloadPage(search="") {
|
|
if (search) {
|
|
location.replace(location.pathname + "?" + search);
|
|
} else {
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
function parseQueryParam() {
|
|
const p = new URLSearchParams(location.search)
|
|
if (p.has("reset")) {
|
|
// drop the query before reloading so that a reload does not reset again
|
|
p.delete("reset");
|
|
fetch("/flipstreamviewer/reset", { method: "POST" }).then(() => location.search = p.toString());
|
|
return;
|
|
}
|
|
if (p.has("px")) {
|
|
document.getElementById("presetXorkeyInputDiv").style.display = "block";
|
|
}
|
|
if (p.has("darker")) {
|
|
onInputDarker(p.get("darker"));
|
|
}
|
|
if (p.has("toggleView")) {
|
|
toggleView();
|
|
}
|
|
if (p.has("run")) {
|
|
updateParam(false, true);
|
|
}
|
|
if (p.has("showPresetDialog")) {
|
|
showPresetDialog();
|
|
}
|
|
if (p.has("presetFolder")) {
|
|
const presetFolder = document.getElementById("presetFolderSelect").value;
|
|
if (presetFolder != p.get("presetFolder")) {
|
|
updateParam(true, false, { presetFolder: p.get("presetFolder") }, {}, p.toString());
|
|
return;
|
|
}
|
|
}
|
|
if (p.has("presetFile")) {
|
|
const presetFile = document.getElementById("presetFileSelect").value;
|
|
if (presetFile != p.get("presetFile")) {
|
|
loadPreset(false, { presetFile: p.get("presetFile") }, p.toString());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
parseQueryParam();
|
|
"""
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/run_once")
|
|
async def run_once(request):
|
|
server.PromptServer.instance.send_sync("FlipStreamViewer_run_once", {})
|
|
global refresh_updating
|
|
refresh_updating = 0
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/stream/info")
|
|
async def stream_count(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
# long-poll: hold the response until the stream changes
|
|
since = request.query.get("since")
|
|
if since is not None:
|
|
try:
|
|
since = float(since)
|
|
timeout = min(float(request.query.get("timeout", 25)), 60)
|
|
except ValueError:
|
|
raise web.HTTPBadRequest(text="Invalid since or timeout")
|
|
deadline = time.monotonic() + timeout
|
|
while frame_mtime == since and time.monotonic() < deadline:
|
|
await asyncio.sleep(0.05)
|
|
|
|
video = stream_video_mtime == frame_mtime and len(stream_video) > 0
|
|
return web.json_response({"mtime": frame_mtime, "fps": frame_fps, "count": len(frame_buffer), "video": video, "audio": video and stream_video_audio})
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/stream/video.mp4")
|
|
async def stream_video_route(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
if not stream_video:
|
|
raise web.HTTPNotFound(text="Video not found")
|
|
|
|
return web.Response(body=stream_video, headers={"Content-Type": "video/mp4"})
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/stream/{frame_id:\\d+}.png")
|
|
async def stream_frame(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
try:
|
|
frame_id = int(request.match_info["frame_id"])
|
|
except (KeyError, ValueError):
|
|
raise web.HTTPBadRequest(text="Invalid frame ID")
|
|
|
|
if not (0 <= frame_id < len(frame_buffer)):
|
|
raise web.HTTPNotFound(text="Frame not found")
|
|
|
|
return web.Response(body=frame_buffer[frame_id], headers={"Content-Type": "image/png"})
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/preview")
|
|
async def preview(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
label = request.query.get('label')
|
|
if not label:
|
|
return web.Response(status=400, text="Label required")
|
|
|
|
key = label + "PreviewBox"
|
|
if key in state:
|
|
data = state[key][1]
|
|
else:
|
|
data = b""
|
|
return web.Response(body=data, headers={"Content-Type": "image/png"})
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/preview_setroi")
|
|
async def preview_setroi(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
data = await request.json()
|
|
param[data['label'] + "PreviewRoi"] = data
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/paste")
|
|
async def paste(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
label = request.query.get('label')
|
|
if not label:
|
|
return web.Response(status=400, text="Label required")
|
|
|
|
if label + "_thumbnail" in state:
|
|
data = state[label + "_thumbnail"]
|
|
else:
|
|
data = b""
|
|
return web.Response(body=data, headers={"Content-Type": "image/png"})
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/paste_upload")
|
|
async def paste_upload(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
label = request.query.get('label')
|
|
if not label:
|
|
return web.Response(status=400, text="Label required")
|
|
|
|
data = await request.read()
|
|
img = Image.open(io.BytesIO(data))
|
|
image = torch.from_numpy(np.array(img)).float()[None, :] / 255.0
|
|
if image.shape[3] == 4:
|
|
image = image[:,:,:,:3] * image[:,:,:,3:4]
|
|
state[label] = image
|
|
state[label + "_thumbnail"] = data
|
|
state[label + "_mtime"] = time.time()
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/paste_remove")
|
|
async def paste_upload(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
label = request.query.get('label')
|
|
if not label:
|
|
return web.Response(status=400, text="Label required")
|
|
|
|
if label in state:
|
|
del state[label]
|
|
del state[label + "_thumbnail"]
|
|
del state[label + "_mtime"]
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer")
|
|
async def viewer(request):
|
|
if request.remote not in allowed_ips:
|
|
print(request.remote)
|
|
raise HTTPForbidden()
|
|
|
|
block = {}
|
|
|
|
def add_section(title, section, closed=False, **_):
|
|
block[f"{title}_{section}"] = f"""
|
|
</details>
|
|
<details{"" if closed else " open"}>
|
|
<summary><i>{section}</i></summary>"""
|
|
|
|
def add_button(title, run, capture, **_):
|
|
block[f"{title}"] = f"""
|
|
<div class="row">"""
|
|
if run:
|
|
block[f"{title}"] += f"""
|
|
<button class="willreload" onclick="updateParam(true, true)">Run</button>"""
|
|
if capture:
|
|
block[f"{title}"] += f"""
|
|
<button onclick="capture()">Capture</button>"""
|
|
block[f"{title}"] += f"""
|
|
</div>"""
|
|
|
|
def add_slider(title, label, default, min, max, step, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
param.setdefault(label, default)
|
|
block[f"{title}_{label}"] = f"""
|
|
<div class="row" style="color: lightslategray;">
|
|
<input class="FlipStreamSlider" id="{label}Slider" type="range" min="{min}" max="{max}" step="{step}" name="{label}" value="{float(param[label]):g}" oninput="{label}Value.innerText = this.value;" />
|
|
<span id="{label}Value">{float(param[label]):g}</span>{label}
|
|
</div>"""
|
|
|
|
def add_textbox(title, label, default, rows, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
param.setdefault(label, btoa_utf8(default))
|
|
block[f"{title}_{label}"] = f"""
|
|
<textarea class="FlipStreamTextBox" id="{label}TextBox" style="color: lightslategray;" placeholder="{label}" rows="{rows}" name="{label}">{atob_utf8(param[label])}</textarea>"""
|
|
|
|
def add_inputbox(title, label, default, boxtype, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
param.setdefault(label, default)
|
|
if boxtype == "seed":
|
|
block[f"{title}_{label}"] = f"""
|
|
<div class="row" style="color: lightslategray;">
|
|
{label}: <input class="FlipStreamInputBox" id="{label}InputBox" style="color: lightslategray;" placeholder="{label}" type="number" name="{label}" value="{param[label]}" />
|
|
<button onclick="{label}InputBox.value=Math.floor(Math.random()*1e7); updateParam(true, true)">R</button>
|
|
</div>"""
|
|
elif boxtype == "r4d":
|
|
block[f"{title}_{label}"] = f"""
|
|
<div class="row" style="color: lightslategray;">
|
|
{label}: <input class="FlipStreamInputBox" id="{label}InputBox" style="color: lightslategray;" placeholder="{label}" type="number" name="{label}" value="{param[label]}" />
|
|
<button onclick="{label}InputBox.value=Math.floor(Math.random()*1e4); updateParam(true, true)">R</button>
|
|
</div>"""
|
|
else:
|
|
block[f"{title}_{label}"] = f"""
|
|
<div class="row" style="color: lightslategray;">
|
|
{label}: <input class="FlipStreamInputBox" id="{label}InputBox" style="color: lightslategray;" placeholder="{label}" type="{boxtype}" name="{label}" value="{param[label]}" />
|
|
<button onclick="updateParam(true, true)">U</button>
|
|
</div>"""
|
|
|
|
def add_selectbox(title, label, listitems, **_):
|
|
listitems = listitems.split(",")
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
if not all(item == html.escape(item) for item in listitems):
|
|
raise RuntimeError(f"{title}: listitems must contain only HTML-acceptable characters.")
|
|
param.setdefault(label, "")
|
|
text_html = f"""
|
|
<select class="FlipStreamSelectBox" id="{label}SelectBox" name="{label}">
|
|
<option value="">{label}</option>"""
|
|
for item in listitems:
|
|
text_html += f"""
|
|
<option value="{item}"{" selected" if param[label] == item else ""}>{item}</option>"""
|
|
text_html += f"""
|
|
</select>"""
|
|
block[f"{title}_{label}"] = text_html
|
|
|
|
def add_sizeselect(title, label, listitems, **_):
|
|
listitems = listitems.split(",")
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
if not all(item == html.escape(item) for item in listitems):
|
|
raise RuntimeError(f"{title}: listitems must contain only HTML-acceptable characters.")
|
|
param.setdefault(label, "")
|
|
text_html = f"""
|
|
<select class="FlipStreamSizeSelect" id="{label}SizeSelect" name="{label}">
|
|
<option value="">{label}</option>"""
|
|
for item in listitems:
|
|
text_html += f"""
|
|
<option value="{item}"{" selected" if param[label] == item else ""}>{item}</option>"""
|
|
text_html += f"""
|
|
</select>"""
|
|
block[f"{title}_{label}"] = text_html
|
|
|
|
def add_fileselect(title, label, folder_name, folder_path, mode, use_sub, use_move, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
if not (mode == "" or mode.isidentifier()):
|
|
raise RuntimeError(f"{title}: mode must contain only valid identifier characters.")
|
|
param.setdefault(label, "")
|
|
state.setdefault(f"{label}Folder", "")
|
|
text_html = ""
|
|
if use_sub:
|
|
text_html += f"""
|
|
<select class="FlipStreamFolderSelect willreload" id="{label}FolderSelect" name="{label}Folder" onchange="updateParam(true)">
|
|
<option value="">{label} folder</option>"""
|
|
for dir in sorted(Path(folder_path, mode).glob("*/")):
|
|
selected = " selected" if state[f"{label}Folder"] == dir.name else ""
|
|
text_html += f"""
|
|
<option value="{dir.name}"{selected}>{dir.name}</option>"""
|
|
text_html += f"""
|
|
</select>"""
|
|
text_html += f"""
|
|
<div class="row">
|
|
<select class="FlipStreamFileSelect" id="{label}FileSelect" name="{label}">
|
|
<option value="">{label}</option>"""
|
|
if param[label]:
|
|
text_html += f"""
|
|
<option value="{param[label]}" selected>*{Path(param[label]).stem}</option>"""
|
|
if use_sub:
|
|
files = sorted(Path(folder_path, mode, state[f"{label}Folder"]).glob("*.*"))
|
|
files = [Path(file).relative_to(folder_path) for file in files]
|
|
else:
|
|
files = [Path(file) for file in FlipStreamFileSelect.get_filelist(folder_name, folder_path, mode)]
|
|
|
|
png_files = [file for file in files if file.suffix.lower() == '.png']
|
|
other_files = [file for file in files if file.suffix.lower() != '.png']
|
|
|
|
for file in other_files:
|
|
if file != param[label]:
|
|
text_html += f"""
|
|
<option value="{file}">{file.stem}</option>"""
|
|
png_file = file.with_suffix('.png')
|
|
if png_file in png_files:
|
|
png_files.remove(png_file)
|
|
for file in png_files:
|
|
if file != param[label]:
|
|
text_html += f"""
|
|
<option value="{file}">{file.stem}</option>"""
|
|
text_html += f"""
|
|
</select>"""
|
|
if use_move:
|
|
text_html += f"""
|
|
<button class="willreload" id="{label}MoveButton" onClick="showMoveFileSelect({label}MoveFileSelect)">M</button>"""
|
|
text_html += f"""
|
|
</div>"""
|
|
if use_move:
|
|
text_html += f"""
|
|
<select class="FlipStreamMoveFileSelect" id="{label}MoveFileSelect" onchange="moveFile(String.raw`{folder_path}`, '{mode}', '{label}')">
|
|
<option value="" disabled selected>move to</option>"""
|
|
for dir in Path(folder_path, mode).glob("*/"):
|
|
text_html += f"""
|
|
<option value="{dir.name}">{dir.name}</option>"""
|
|
text_html += f"""
|
|
</select>"""
|
|
block[f"{title}_{label}"] = text_html
|
|
|
|
def add_previewbox(title, label, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
if (label + "PreviewBox") in state:
|
|
block[f"{title}_{label}"] = f"""
|
|
<div class="row" style="color: lightslategray;">
|
|
<img class="FlipStreamPreviewBox" id="{label}PreviewBox" name="{label}" src="/flipstreamviewer/preview?label={label}" alt onerror="this.onerror = null; this.src='';" />
|
|
<canvas class="FlipStreamPreviewRoi" id="{label}PreviewRoi"></canvas>
|
|
</div>"""
|
|
|
|
def add_pastebox(title, label, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
block[f"{title}_{label}"] = f"""
|
|
<div class="row" style="color: lightslategray;">
|
|
<img class="FlipStreamPasteBox" id="{label}PasteBox" name="{label}" src="/flipstreamviewer/paste?label={label}" alt="{label}" />
|
|
</div>"""
|
|
|
|
def add_logbox(title, label, rows, **_):
|
|
if not label.isidentifier():
|
|
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
|
|
if (label + "LogBox") in state:
|
|
block[f"{title}_{label}"] = f"""
|
|
<textarea class="FlipStreamLogBox" id="{label}LogBox" style="color: lightslategray;" placeholder="{label}" rows="{rows}" name="{label}" readonly></textarea>"""
|
|
|
|
info = server.PromptServer.instance.prompt_queue.get_history(max_items=1, map_function=lambda p: p["prompt"][2])
|
|
nodelist = next(iter(info.values())).values() if info else []
|
|
if nodelist:
|
|
for node in nodelist:
|
|
class_type = node["class_type"]
|
|
title = node.get("_meta", {}).get("title", class_type)
|
|
inputs = node["inputs"]
|
|
if class_type == "FlipStreamSection":
|
|
add_section(title, **inputs)
|
|
if class_type == "FlipStreamButton":
|
|
add_button(title, **inputs)
|
|
if class_type == "FlipStreamSlider":
|
|
add_slider(title, **inputs)
|
|
if class_type == "FlipStreamTextBox":
|
|
add_textbox(title, **inputs)
|
|
if class_type == "FlipStreamInputBox":
|
|
add_inputbox(title, **inputs)
|
|
if class_type.startswith("FlipStreamSelectBox"):
|
|
add_selectbox(title, **inputs)
|
|
if class_type.startswith("FlipStreamSizeSelect"):
|
|
add_sizeselect(title, **inputs)
|
|
if class_type.startswith("FlipStreamFileSelect"):
|
|
add_fileselect(title, **inputs)
|
|
if class_type == "FlipStreamPreviewBox":
|
|
add_previewbox(title, **inputs)
|
|
if class_type == "FlipStreamPasteBox":
|
|
add_pastebox(title, **inputs)
|
|
if class_type == "FlipStreamLogBox":
|
|
add_logbox(title, **inputs)
|
|
|
|
text_html = f"""<html>{HEAD}<body>
|
|
<div id="mainDialog">
|
|
<div id="leftPanel">
|
|
<div class="row">
|
|
<button class="willreload" onclick="updateParam(true, true)">Run</button>
|
|
<span id="streamAudioRow" style="display: none;">
|
|
<button id="muteButton" onclick="toggleMute()">🔇</button>
|
|
<input id="volumeRange" type="range" min="0" max="100" value="0" style="flex: 1; min-width: 0;" oninput="setVolume(this.value)" />
|
|
</span>
|
|
</div>
|
|
<details open>
|
|
<summary><i>Input</i></summary>
|
|
{"".join([x[1] for x in sorted(block.items())])}
|
|
</details>
|
|
</div>
|
|
<div id="centerPanel" onclick="toggleView()">
|
|
<div id="messageBox"></div>
|
|
</div>
|
|
<div id="rightPanel">
|
|
<div class="row" id="statusSummary"></div>
|
|
<progress id="statusLastElapsed" max="120" value="{state["lastElapsed"]}" style="width: 100%;"></progress>
|
|
<progress id="statusElapsed" max="120" value="0" style="width: 100%;"></progress>
|
|
<details>
|
|
<summary><i>Status</i></summary>
|
|
<textarea id="statusInfo" style="color: lightslategray;" rows="5"></textarea>
|
|
<div class="row">
|
|
<input id="darkerRange" type="range" min="0" max="1" step="0.01" value="{state["darker"]}" oninput="onInputDarker();" />
|
|
<span id="darkerValue">{state["darker"]}</span>drk
|
|
</div>
|
|
</details>
|
|
<details>
|
|
<summary><i>Preset</i></summary>
|
|
<select id="presetFolderSelect" class="willreload" onchange="updateParam(true)">
|
|
<option value="" 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>
|
|
<div class="row">
|
|
<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>
|
|
<button 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 onclick="savePreset()">Save</button>
|
|
</div>
|
|
<div id="presetXorkeyInputDiv">
|
|
<input id="presetXorkeyInput" type="password" placeholder="xorkey" />
|
|
</div>
|
|
<div class="row">
|
|
<button onclick="showPresetDialog()">Choose</button>
|
|
<button class="willreload" onclick="loadPreset()">Load</button>
|
|
<button class="willreload" onclick="loadPreset(true)">LoraOnly</button>
|
|
</div>
|
|
</details>
|
|
<details open>
|
|
<summary><i>Lora</i></summary>
|
|
<select id="loraModeSelect" class="willreload" onchange="updateParam(true)">
|
|
<option value="" selected>lora mode</option>
|
|
{"".join([f'<option value="{dir.name}"{" selected" if state["loraMode"] == dir.name else ""}>{dir.name}</option>' for dir in Path("ComfyUI/models/loras").glob("*/")])}
|
|
</select>
|
|
<select id="loraFolderSelect" class="willreload" onchange="updateParam(true)">
|
|
<option value="" 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["loraMode"]).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["loraMode"], state["loraFolder"]).glob("*.safetensors")])}
|
|
</select>
|
|
<button onclick="toggleLora()">T</button>
|
|
<button 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["loraMode"]).glob("*/")])}
|
|
</select>
|
|
<div class="row">
|
|
<select id="loraTagSelect" onchange="toggleTag()">
|
|
<option value="">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 onclick="toggleTag()">T</button>
|
|
<button 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">
|
|
<button onclick="showTagDialog()">Choose</button>
|
|
<button onclick="updateParam(true, true)">Run</button>
|
|
<button onclick="clearLoraInput()">Clr</button>
|
|
<button onclick="showTagDialog();tagCSel();tagRSel();tagOK();updateParam(true, true)">R</button>
|
|
</div>
|
|
<textarea id="loraInput" placeholder="Enter lora" rows="12">{atob_utf8(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>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
<div id="captureDialog">
|
|
<div id="captureLeftPanel">
|
|
</div>
|
|
<div id="captureCenterPanel">
|
|
<canvas id="canvas" width="960" height="600" />
|
|
</div>
|
|
<div id="captureRightPanel">
|
|
<div class="row">
|
|
<button onclick="resetPos()">Reset</button>
|
|
</div>
|
|
<div class="row">
|
|
X <input id="offsetXRange" type="range" min="-960" max="960" value="{param["_capture_offsetX"]}" onchange="updateCanvas()">
|
|
</div>
|
|
<div class="row">
|
|
Y <input id="offsetYRange" type="range" min="-600" max="600" value="{param["_capture_offsetY"]}" onchange="updateCanvas()">
|
|
</div>
|
|
<div class="row">
|
|
S <input id="scaleRange" type="range" min="50" max="400" value="{param["_capture_scale"]}" onchange="updateCanvas()">
|
|
</div>
|
|
<div class="row">
|
|
<button onclick="setFrame()">SetFrame</button>
|
|
<button onclick="closeCaptureDialog()">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="tagDialog">
|
|
<div id="tagLeftPanel">
|
|
</div>
|
|
<div id="tagCenterPanel">
|
|
</div>
|
|
<div id="tagRightPanel">
|
|
<div class="row">
|
|
<button onclick="tagCSel()">CSel</button>
|
|
<button onclick="tagRSel()">RSel</button>
|
|
</div>
|
|
<div class="row">
|
|
<button onclick="tagOK()">OK</button>
|
|
<button onclick="closeTagDialog()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="presetDialog">
|
|
<div id="presetLeftPanel">
|
|
</div>
|
|
<div id="presetCenterPanel" onclick="toggleView()">
|
|
<div id="messageBox"></div>
|
|
</div>
|
|
<div id="presetRightPanel">
|
|
<div class="row">
|
|
<button onclick="closePresetDialog()">Close</button>
|
|
</div>
|
|
<div class="row"><i>Status</i></div>
|
|
<div id="statusInfo"></div>
|
|
<div class="row"><i>Darker</i></div>
|
|
<div class="row">
|
|
<input id="darkerRange" type="range" min="0" max="1" step="0.01" value="{state["darker"]}" oninput="onInputDarker(this.value);" />
|
|
<span id="darkerValue">{state["darker"]}</span>drk
|
|
</div>
|
|
<div class="row"><i>Preset</i></div>
|
|
{"".join([f'''
|
|
<button onclick="loadPreset(false, {{ presetFile: '{file.name}' }}, 'showPresetDialog')">{file.stem}</button>
|
|
''' for file in Path("preset", state["presetFolder"]).glob("*.json")])}
|
|
</div>
|
|
</div>
|
|
<script>
|
|
{SCRIPT_PARAM}
|
|
{SCRIPT_PRESET}
|
|
{SCRIPT_CKPT}
|
|
{SCRIPT_LORA}
|
|
{SCRIPT_TAG}
|
|
{SCRIPT_VIEW}
|
|
{SCRIPT_CAPTURE}
|
|
{SCRIPT_QUERY}
|
|
</script>
|
|
</body></html>"""
|
|
return web.Response(text=text_html, content_type="text/html")
|
|
|
|
|
|
@server.PromptServer.instance.routes.get("/flipstreamviewer/refresh_view")
|
|
async def get_status(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
status_elapsed = 0
|
|
status_summary = ""
|
|
status_info = []
|
|
|
|
remain = server.PromptServer.instance.prompt_queue.get_tasks_remaining()
|
|
global refresh_updating
|
|
if remain == 0:
|
|
refresh_updating = 0
|
|
elif refresh_updating == 0:
|
|
refresh_updating = time.time()
|
|
|
|
if refresh_updating:
|
|
status_elapsed = int(time.time() - refresh_updating)
|
|
|
|
if "current" in state:
|
|
status_summary = state["current"]
|
|
|
|
status_info.append(f"q{remain}")
|
|
|
|
info = server.PromptServer.instance.prompt_queue.get_history(max_items=1, map_function=lambda p: p["status"])
|
|
status = next(iter(info.values())) if info else []
|
|
if status:
|
|
errinfo = status["messages"][2][1]
|
|
has_error = errinfo.get("exception_message", "") != ""
|
|
if status["status_str"] != "error" or has_error:
|
|
status_info.append(status["status_str"])
|
|
if has_error:
|
|
status_summary += " E"
|
|
status_info += [errinfo[key] for key in ["node_id", "node_type", "exception_message", "exception_type"] if key in errinfo]
|
|
|
|
data = refresh_data.copy()
|
|
data["param"] = refresh_param.copy()
|
|
param.update(refresh_param)
|
|
refresh_param.clear()
|
|
data["status_elapsed"] = status_elapsed
|
|
data["status_summary"] = status_summary
|
|
data["status_info"] = status_info
|
|
data["preview_mtime"] = {key: state[key][0] for key in state if key.endswith("PreviewBox")}
|
|
data["mtime"] = {key: state[key] for key in state if key.endswith("_mtime")}
|
|
data["log"] = {key: state[key] for key in state if key.endswith("LogBox")}
|
|
return web.json_response(data)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/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)
|
|
time.sleep(UPDATE_DELAY)
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/reset")
|
|
async def reset(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
try:
|
|
server.PromptServer.instance.prompt_queue.wipe_queue()
|
|
comfy.model_management.interrupt_current_processing()
|
|
except Exception as e:
|
|
print(f"FlipStreamViewer: reset failed to clear queue: {e}")
|
|
|
|
state.clear()
|
|
state.update(default_state)
|
|
state["presetTitle"] = time.strftime("%Y%m%d-%H%M")
|
|
param.clear()
|
|
param.update(default_param)
|
|
refresh_data.clear()
|
|
refresh_param.clear()
|
|
|
|
global frame_buffer
|
|
global frame_mtime
|
|
global setframe_mtime
|
|
global setframe_buffer
|
|
global stream_video
|
|
frame_buffer = []
|
|
setframe_buffer = []
|
|
stream_video = b""
|
|
frame_mtime = time.time()
|
|
setframe_mtime = frame_mtime
|
|
|
|
time.sleep(UPDATE_DELAY)
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/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["loraMode"], 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()
|
|
if "modelId" in res:
|
|
lorainfo["_lorapreview_href"] = "https://civitai.com/models/" + str(res["modelId"])
|
|
if res.get("images"):
|
|
lorainfo["_lorapreview_src"] = res["images"][0]["url"]
|
|
|
|
return web.json_response(lorainfo)
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/set_frame")
|
|
async def set_frame(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
frame = base64.b64decode((await request.text()).split(',', 1)[1])
|
|
global frame_buffer
|
|
global frame_mtime
|
|
global setframe_mtime
|
|
global setframe_buffer
|
|
frame_buffer = [frame]
|
|
frame_mtime = time.time()
|
|
setframe_buffer = [frame]
|
|
setframe_mtime = frame_mtime
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/move_file")
|
|
async def move_file(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, folder_path, mode, label, moveFrom, moveTo = await request.json()
|
|
state.update(stt)
|
|
pathFrom = Path(folder_path, mode, state[label + "Folder"], Path(moveFrom).name)
|
|
pathTo = Path(folder_path, mode, moveTo, pathFrom.name)
|
|
Path(pathFrom).rename(pathTo)
|
|
state[label + "Folder"] = moveTo
|
|
return web.Response()
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/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()
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/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["loraMode"], state["loraFolder"], state["loraFile"])
|
|
pathTo = Path("ComfyUI/models/loras", state["loraMode"], moveTo, pathFrom.name)
|
|
Path(pathFrom).rename(pathTo)
|
|
state["loraFolder"] = moveTo
|
|
return web.Response()
|
|
|
|
|
|
def xor_crypt(data_str, key_str, crypt=True):
|
|
xorall = lambda data, key: bytes([b1 ^ b2 for b1, b2 in zip(data, itertools.cycle(key))])
|
|
key_bytes = key_str.encode('utf-8')
|
|
input_bytes = data_str.encode('utf-8')
|
|
if crypt:
|
|
encrypted_bytes = xorall(input_bytes, key_bytes)
|
|
return base64.b64encode(encrypted_bytes).decode('utf-8')
|
|
else:
|
|
decoded_bytes = base64.b64decode(input_bytes)
|
|
return xorall(decoded_bytes, key_bytes).decode('utf-8')
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/load_preset")
|
|
async def load_preset(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, loraPromptOnly, xorkey = await request.json()
|
|
filename = stt["presetFile"]
|
|
path = Path("preset", stt["presetFolder"], filename)
|
|
with open(path, "r") as file:
|
|
if xorkey:
|
|
buf = json.loads(xor_crypt(file.read(), xorkey + filename, False))
|
|
else:
|
|
buf = json.load(file)
|
|
if loraPromptOnly:
|
|
buf = {"lora": buf.get("lora", "")}
|
|
|
|
state.update(stt)
|
|
param.clear()
|
|
param.update(default_param)
|
|
param.update(buf)
|
|
state["presetTitle"] = Path(state["presetFile"]).stem
|
|
time.sleep(UPDATE_DELAY)
|
|
return web.json_response({"lora": param["lora"], "presetTitle": state["presetTitle"]})
|
|
|
|
|
|
@server.PromptServer.instance.routes.post("/flipstreamviewer/save_preset")
|
|
async def save_preset(request):
|
|
if request.remote not in allowed_ips:
|
|
raise HTTPForbidden()
|
|
|
|
stt, prm, xorkey = await request.json()
|
|
state.update(stt)
|
|
param.update(prm)
|
|
time.sleep(UPDATE_DELAY)
|
|
|
|
filename = state["presetTitle"] + ".json"
|
|
path = Path("preset", state["presetFolder"], filename)
|
|
with open(path, "w") as file:
|
|
if xorkey:
|
|
file.write(xor_crypt(json.dumps(param), xorkey + filename))
|
|
else:
|
|
json.dump(param, file)
|
|
return web.Response()
|
|
|
|
|
|
class FlipStreamSection:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"section": ("STRING", {"default": "Section"}),
|
|
"closed": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, hook=True, **_):
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamButton:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"run": ("BOOLEAN", {"default": True}),
|
|
"capture": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, hook=True, **_):
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamSlider:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": ("FLOAT", {"default": 0, "step": 0.01, "round": 0.01}),
|
|
"min": ("FLOAT", {"default": 0, "step": 0.01, "round": 0.01}),
|
|
"max": ("FLOAT", {"default": 0, "step": 0.01, "round": 0.01}),
|
|
"step": ("FLOAT", {"default": 0.01, "min": 0.00, "step": 0.01, "round": 0.01}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("FLOAT", "INT", "BOOLEAN")
|
|
RETURN_NAMES = ("float", "int", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, default, **_):
|
|
param.setdefault(label, default)
|
|
return hash(param[label])
|
|
|
|
def run(self, label, default, **_):
|
|
param.setdefault(label, default)
|
|
return (float(param[label]), int(float(param[label])), bool(float(param[label])))
|
|
|
|
|
|
class FlipStreamTextBox:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": ("STRING", {"default": "", "multiline": True}),
|
|
"rows": ("INT", {"default": 3}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "BOOLEAN")
|
|
RETURN_NAMES = ("text", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, default, **_):
|
|
param.setdefault(label, btoa_utf8(default))
|
|
return hash(param[label])
|
|
|
|
def run(self, label, default, **_):
|
|
param.setdefault(label, btoa_utf8(default))
|
|
text = atob_utf8(param[label])
|
|
return (text, bool(text))
|
|
|
|
|
|
class FlipStreamInputBox:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": ("STRING", {"default": ""}),
|
|
"boxtype": (["text", "number", "seed", "r4d"],),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "FLOAT", "INT", "BOOLEAN")
|
|
RETURN_NAMES = ("text", "float", "int", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, default, **_):
|
|
return hash(param.get(label))
|
|
|
|
def run(self, label, default, boxtype, **_):
|
|
param.setdefault(label, default)
|
|
text = param[label]
|
|
enable = bool(text)
|
|
try:
|
|
num = float(text)
|
|
enable = bool(num)
|
|
except:
|
|
num = 0
|
|
return (text, num, int(num), enable)
|
|
|
|
|
|
class FlipStreamSelectBox:
|
|
LISTITEMS = ["a", "b", "c"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": (s.LISTITEMS,),
|
|
"listitems": ("STRING", {"default": ",".join(s.LISTITEMS)})
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (anytype, "BOOLEAN",)
|
|
RETURN_NAMES = ("item", "enable",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, **_):
|
|
param.setdefault(label, "")
|
|
return hash(param[label])
|
|
|
|
def run(self, label, default, **_):
|
|
param.setdefault(label, "")
|
|
item = param[label] if param[label] else default
|
|
return (item, param[label] != "")
|
|
|
|
|
|
class FlipStreamSelectBox_Samplers(FlipStreamSelectBox):
|
|
LISTITEMS = comfy.samplers.KSampler.SAMPLERS
|
|
|
|
|
|
class FlipStreamSelectBox_Scheduler(FlipStreamSelectBox):
|
|
LISTITEMS = comfy.samplers.KSampler.SCHEDULERS
|
|
|
|
|
|
class FlipStreamSizeSelect:
|
|
SIZEDICT = {
|
|
"sdxl": {
|
|
"1:1": (1024, 1024),
|
|
"16:9": (1344, 768),
|
|
"9:16": (768, 1344),
|
|
"4:3": (1152, 896),
|
|
"3:4": (896, 1152),
|
|
"3:2": (1216, 832),
|
|
"2:3": (832, 1216),
|
|
},
|
|
"qwen": {
|
|
"1:1": (1328, 1328),
|
|
"16:9": (1664, 928),
|
|
"9:16": (928, 1664),
|
|
"4:3": (1472, 1104),
|
|
"3:4": (1104, 1472),
|
|
"3:2": (1584, 1056),
|
|
"2:3": (1056, 1584),
|
|
},
|
|
"krea2": {
|
|
"1:1": (1024, 1024),
|
|
"16:9": (1376, 768),
|
|
"9:16": (768, 1376),
|
|
"4:3": (1184, 896),
|
|
"3:4": (896, 1184),
|
|
"3:2": (1248, 832),
|
|
"2:3": (832, 1248),
|
|
},
|
|
"wan": {
|
|
"1:1": (512, 512),
|
|
"16:9": (832, 480),
|
|
"9:16": (480, 832),
|
|
"4:3": (832, 480),
|
|
"3:4": (480, 832),
|
|
"3:2": (832, 480),
|
|
"2:3": (480, 832),
|
|
},
|
|
"ltxv": {
|
|
"1:1": (736, 736),
|
|
"16:9": (960, 544),
|
|
"9:16": (544, 960),
|
|
"4:3": (832, 608),
|
|
"3:4": (608, 832),
|
|
"3:2": (864, 576),
|
|
"2:3": (576, 864),
|
|
},
|
|
"ltxv_768": {
|
|
"1:1": (768, 768),
|
|
"16:9": (1344, 768),
|
|
"9:16": (448, 768),
|
|
"4:3": (1024, 768),
|
|
"3:4": (576, 768),
|
|
"3:2": (1152, 768),
|
|
"2:3": (512, 768),
|
|
},
|
|
"h3_768": {
|
|
"1:1": (768, 768),
|
|
"16:9": (1344, 768),
|
|
"9:16": (448, 768),
|
|
"4:3": (1024, 768),
|
|
"3:4": (576, 768),
|
|
"3:2": (1152, 768),
|
|
"2:3": (512, 768),
|
|
},
|
|
"h3_384": {
|
|
"1:1": (384, 384),
|
|
"16:9": (672, 384),
|
|
"9:16": (224, 384),
|
|
"4:3": (512, 384),
|
|
"3:4": (288, 384),
|
|
"3:2": (576, 384),
|
|
"2:3": (256, 384),
|
|
},
|
|
"h3_192": {
|
|
"1:1": (192, 192),
|
|
"16:9": (352, 192),
|
|
"9:16": (128, 192),
|
|
"4:3": (256, 192),
|
|
"3:4": (128, 192),
|
|
"3:2": (288, 192),
|
|
"2:3": (128, 192),
|
|
}
|
|
}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": (list(s.SIZEDICT["sdxl"].keys()),),
|
|
"modeltype": (list(s.SIZEDICT.keys()),),
|
|
"listitems": ("STRING", {"default": ",".join(s.SIZEDICT["sdxl"].keys())})
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("INT", "INT", "BOOLEAN")
|
|
RETURN_NAMES = ("width", "height", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, default, **_):
|
|
param.setdefault(label, default)
|
|
return hash(param[label])
|
|
|
|
def run(self, label, default, modeltype, **_):
|
|
param.setdefault(label, default)
|
|
key = param[label]
|
|
enable = key != ""
|
|
if key not in self.SIZEDICT[modeltype]:
|
|
key = default
|
|
width, height = self.SIZEDICT[modeltype][key]
|
|
return (width, height, enable)
|
|
|
|
|
|
class FlipStreamGetSize(FlipStreamSizeSelect):
|
|
pass
|
|
|
|
|
|
class FlipStreamFileSelect:
|
|
FOLDER_NAME = ""
|
|
FOLDER_PATH = ""
|
|
|
|
@staticmethod
|
|
def get_filelist(folder_name, folder_path, mode):
|
|
if folder_name == "checkpoints" and not mode:
|
|
return CheckpointLoaderSimple.INPUT_TYPES()["required"]["ckpt_name"][0]
|
|
elif folder_name == "diffusion_models" and not mode:
|
|
return folder_paths.get_filename_list(folder_name)
|
|
elif folder_name == "vae":
|
|
return VAELoader.INPUT_TYPES()["required"]["vae_name"][0]
|
|
elif folder_name == "controlnet":
|
|
return folder_paths.get_filename_list("controlnet")
|
|
elif folder_name == "tensorrt":
|
|
if folder_path.startswith("_error_"):
|
|
return [folder_path]
|
|
if TensorRTLoader is None:
|
|
return ["_error_ ComfyUI_TensorRT is not installed"]
|
|
return TensorRTLoader.INPUT_TYPES()["required"]["unet_name"][0]
|
|
elif folder_name == "animatediff_models":
|
|
if folder_path.startswith("_error_"):
|
|
return [folder_path]
|
|
return folder_paths.get_filename_list(folder_name)
|
|
else:
|
|
return [str(p.relative_to(folder_path)) for p in Path(folder_path, mode).glob("*.*")]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": ([""] + s.get_filelist(s.FOLDER_NAME, s.FOLDER_PATH, ""),),
|
|
"folder_name": ([s.FOLDER_NAME],),
|
|
"folder_path": ([s.FOLDER_PATH],),
|
|
"mode": ("STRING", {"default": ""}),
|
|
"use_sub": ("BOOLEAN", {"defalut": False}),
|
|
"use_move": ("BOOLEAN", {"defalut": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype, anytype, "BOOLEAN",)
|
|
RETURN_NAMES = ("file", "path", "enable",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, **_):
|
|
param.setdefault(label, "")
|
|
return hash(param[label])
|
|
|
|
def run(self, label, default, folder_path, **_):
|
|
param.setdefault(label, "")
|
|
file = param[label] if param[label] else default
|
|
return (file, str(Path(folder_path, file)), param[label] != "")
|
|
|
|
|
|
class FlipStreamFileSelect_Checkpoints(FlipStreamFileSelect):
|
|
FOLDER_NAME = "checkpoints"
|
|
FOLDER_PATH = Path(folder_paths.get_folder_paths(FOLDER_NAME)[0]).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_DiffusionModels(FlipStreamFileSelect):
|
|
FOLDER_NAME = "diffusion_models"
|
|
# "diffusion_models" also lists the legacy "unet" path
|
|
_paths = folder_paths.get_folder_paths(FOLDER_NAME)
|
|
FOLDER_PATH = Path(([p for p in _paths if p.endswith("diffusion_models")] + _paths)[0]).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_Loras(FlipStreamFileSelect):
|
|
FOLDER_NAME = "loras"
|
|
FOLDER_PATH = Path(folder_paths.get_folder_paths(FOLDER_NAME)[0]).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_VAE(FlipStreamFileSelect):
|
|
FOLDER_NAME = "vae"
|
|
FOLDER_PATH = Path(folder_paths.get_folder_paths(FOLDER_NAME)[0]).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_LLM(FlipStreamFileSelect):
|
|
FOLDER_NAME = "LLM"
|
|
FOLDER_PATH = Path(folder_paths.models_dir, FOLDER_NAME).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_ControlNetModel(FlipStreamFileSelect):
|
|
FOLDER_NAME = "controlnet"
|
|
FOLDER_PATH = Path(folder_paths.get_folder_paths(FOLDER_NAME)[0]).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_TensorRT(FlipStreamFileSelect):
|
|
FOLDER_NAME = "tensorrt"
|
|
try:
|
|
FOLDER_PATH = Path(folder_paths.get_folder_paths(FOLDER_NAME)[0]).relative_to(Path.cwd()).as_posix()
|
|
except:
|
|
FOLDER_PATH = "_error_ tensorrt folder is not found"
|
|
|
|
|
|
class FlipStreamFileSelect_AnimateDiffModel(FlipStreamFileSelect):
|
|
FOLDER_NAME = "animatediff_models"
|
|
try:
|
|
FOLDER_PATH = Path(folder_paths.get_folder_paths(FOLDER_NAME)[0]).relative_to(Path.cwd()).as_posix()
|
|
except:
|
|
FOLDER_PATH = "_error_ animatediff_models folder is not found"
|
|
|
|
|
|
class FlipStreamFileSelect_Input(FlipStreamFileSelect):
|
|
FOLDER_NAME = "input"
|
|
FOLDER_PATH = Path(folder_paths.input_directory).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamFileSelect_Output(FlipStreamFileSelect):
|
|
FOLDER_NAME = "output"
|
|
FOLDER_PATH = Path(folder_paths.output_directory).relative_to(Path.cwd()).as_posix()
|
|
|
|
|
|
class FlipStreamPreviewBox:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"tensor": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, label, tensor, **_):
|
|
if tensor is None:
|
|
tensor = torch.zeros((1, 8, 8, 3))
|
|
buf = np.array(tensor[0].cpu().numpy() * 255, dtype=np.uint8)
|
|
image = Image.fromarray(buf)
|
|
image.thumbnail((256, 256))
|
|
with io.BytesIO() as output:
|
|
image.save(output, format="PNG", compress_level=STREAM_COMPRESSION)
|
|
state[label + "PreviewBox"] = (time.time(), output.getvalue())
|
|
return ()
|
|
|
|
|
|
class FlipStreamPasteBox:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "BOOLEAN")
|
|
RETURN_NAMES = ("image", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label):
|
|
return hash(state.get(label + "_mtime"))
|
|
|
|
def run(self, label, **_):
|
|
if label not in state:
|
|
return (None, False)
|
|
return (state[label], True)
|
|
|
|
|
|
class FlipStreamLogBox:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"log": ("STRING",),
|
|
"rows": ("INT", {"default": 3}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, label, log, **_):
|
|
state[label + "LogBox"] = btoa_utf8(log or "")
|
|
return ()
|
|
|
|
|
|
class FlipStreamRunOnce:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, hook=True, **_):
|
|
server.PromptServer.instance.send_sync("FlipStreamViewer_run_once", {})
|
|
global refresh_updating
|
|
refresh_updating = 0
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamSetMessage:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"message": ("STRING", {"default": "", "multiline": True}),
|
|
"fontsize": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.1}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, message, fontsize, hook=True, **_):
|
|
refresh_data["message"] = btoa_utf8(message)
|
|
refresh_data["message_fontsize"] = f"{fontsize}rem"
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamAnd:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {},
|
|
"optional": {f"v{i}": (anytype,) for i in range(10)}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, **kwargs):
|
|
res = None
|
|
for res in kwargs.values():
|
|
if not res: break
|
|
return (res,)
|
|
|
|
|
|
class FlipStreamOr:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {},
|
|
"optional": {f"v{i}": (anytype,) for i in range(10)}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, **kwargs):
|
|
res = None
|
|
for res in kwargs.values():
|
|
if res: break
|
|
return (res,)
|
|
|
|
|
|
class FlipStreamSetState:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"replace": ("BOOLEAN", {"default": False}),
|
|
"thumbnail": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
"value": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, label, replace, thumbnail, hook=True, value=None, **_):
|
|
if replace or label not in state:
|
|
if thumbnail and value is not None:
|
|
buf = np.array(value[0].cpu().numpy() * 255, dtype=np.uint8)
|
|
image = Image.fromarray(buf)
|
|
image.thumbnail((256, 256))
|
|
with io.BytesIO() as output:
|
|
image.save(output, format="PNG", compress_level=STREAM_COMPRESSION)
|
|
state[label + "_thumbnail"] = output.getvalue()
|
|
state[label + "_mtime"] = time.time()
|
|
else:
|
|
if label + "_thumbnail" in state:
|
|
del state[label + "_thumbnail"]
|
|
del state[label + "_mtime"]
|
|
state[label] = value
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamGetState:
|
|
CACHED_LASTGET = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
"hidden": {
|
|
"unique_id": "UNIQUE_ID"
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype, "BOOLEAN", "BOOLEAN")
|
|
RETURN_NAMES = ("value", "enable", "changed")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, **_):
|
|
value = state.get(label)
|
|
return hash(value[:1024] if hasattr(value, '__getitem__') else value)
|
|
|
|
def run(self, label, unique_id, **_):
|
|
enable = label in param
|
|
value = state.get(label)
|
|
|
|
changed = False
|
|
h = hash(str(value[:1024]) if hasattr(value, '__getitem__') else value)
|
|
cache = FlipStreamGetState.CACHED_LASTGET.setdefault(unique_id, {})
|
|
if cache.get(label, h) != h:
|
|
changed = True
|
|
cache[label] = h
|
|
return (value, enable, changed)
|
|
|
|
|
|
class FlipStreamSetParam:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"value": ("STRING", {"default": "", "multiline": True}),
|
|
"replace": ("BOOLEAN", {"default": False}),
|
|
"b64enc": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, label, value, replace, b64enc, hook=True, **_):
|
|
empty = ""
|
|
if b64enc:
|
|
value = btoa_utf8(value)
|
|
empty = btoa_utf8(empty)
|
|
if replace or label not in param or param[label] == empty:
|
|
param[label] = value
|
|
refresh_param[label] = value
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamGetParam:
|
|
CACHED_LASTGET = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"default": ("STRING", {"default": ""}),
|
|
"b64dec": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
"hidden": {
|
|
"unique_id": "UNIQUE_ID"
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype, "FLOAT", "INT", "BOOLEAN", "BOOLEAN")
|
|
RETURN_NAMES = ("text", "float", "int", "enable", "changed")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, **_):
|
|
return hash(param.get(label))
|
|
|
|
def __init__(self):
|
|
self.lastget = {}
|
|
|
|
def run(self, label, default, b64dec, unique_id, **_):
|
|
enable = label in param
|
|
text = param.get(label, default)
|
|
if enable and b64dec:
|
|
text = atob_utf8(text)
|
|
enable = bool(text)
|
|
try:
|
|
num = float(text)
|
|
enable = bool(num)
|
|
except:
|
|
num = 0
|
|
|
|
changed = False
|
|
h = hash(text)
|
|
cache = FlipStreamGetParam.CACHED_LASTGET.setdefault(unique_id, {})
|
|
if cache.get(label, h) != h:
|
|
changed = True
|
|
cache[label] = h
|
|
return (text, num, int(num), enable, changed)
|
|
|
|
|
|
class FlipStreamGet:
|
|
CACHED_LASTGET = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
**{f"label{i}": ("STRING", {"default": ""}) for i in range(20)}
|
|
},
|
|
"hidden": {
|
|
"unique_id": "UNIQUE_ID"
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype, "BOOLEAN") + (anytype,) * 20
|
|
RETURN_NAMES = ("hook", "changed") + tuple(f"value{i}" for i in range(20))
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
values = []
|
|
for k, label in kwargs.items():
|
|
if not k.startswith("label") or not label:
|
|
continue
|
|
label, _, _ = label.partition("->")
|
|
label, sep, default = label.partition("|")
|
|
if not sep:
|
|
default = None
|
|
source = param
|
|
if label.startswith("state:"):
|
|
_, _, label = label.partition(":")
|
|
source = state
|
|
value = source.get(label, default)
|
|
if isinstance(value, (str, bytes, list, tuple)):
|
|
value = value[:1024]
|
|
values.append(str(value))
|
|
return hash(tuple(values))
|
|
|
|
def get_value(self, label, unique_id):
|
|
def auto(v):
|
|
try:
|
|
return atob_utf8(v)
|
|
except:
|
|
return v
|
|
|
|
convert_map = {
|
|
"": auto,
|
|
"text": str,
|
|
"bool": bool,
|
|
"int": int,
|
|
"float": float,
|
|
"b64dec": atob_utf8,
|
|
}
|
|
|
|
convert = ""
|
|
source = param
|
|
label, _, convert = label.partition("->")
|
|
label, sep, default = label.partition("|")
|
|
if not sep: default = None
|
|
if label.startswith("state:"):
|
|
_, _, label = label.partition(":")
|
|
source = state
|
|
|
|
value = None
|
|
changed = False
|
|
if label:
|
|
value = convert_map[convert](source.get(label, default))
|
|
cache = FlipStreamGet.CACHED_LASTGET.setdefault(unique_id, {})
|
|
h = hash(str(value[:1024]) if hasattr(value, '__getitem__') else value)
|
|
if cache.get(label, h) != h:
|
|
changed = True
|
|
cache[label] = h
|
|
|
|
return value, changed
|
|
|
|
def run(self, unique_id, hook=True, **kwargs):
|
|
vlist = []
|
|
changed = False
|
|
for k in kwargs:
|
|
if k.startswith("label"):
|
|
val, c = self.get_value(kwargs[k], unique_id)
|
|
vlist.append(val)
|
|
changed |= c
|
|
return (hook, changed, *vlist)
|
|
|
|
|
|
class FlipStreamGetPreviewRoi:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "empty"}),
|
|
"width": ("INT", {"default": 512, "min": 256, "max": 2048, "step": 32}),
|
|
"height": ("INT", {"default": 512, "min": 256, "max": 2048, "step": 32}),
|
|
"default_left": ("INT", {"default": 0}),
|
|
"default_top": ("INT", {"default": 0}),
|
|
"default_right": ("INT", {"default": 0}),
|
|
"default_bottom": ("INT", {"default": 0}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("INT", "INT", "INT", "INT", "INT", "INT")
|
|
RETURN_NAMES = ("left", "top", "right", "bottom", "inner_width", "inner_height")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, label, **_):
|
|
roi_data = frozenset(param.get(label + "PreviewRoi", {}).items())
|
|
return hash(roi_data)
|
|
|
|
def run(self, label, default_left, default_top, default_right, default_bottom, width, height):
|
|
roi_data = param.get(label + "PreviewRoi", {})
|
|
left = int(roi_data['sx'] * width) if 'sx' in roi_data else default_left
|
|
top = int(roi_data['sy'] * height) if 'sy' in roi_data else default_top
|
|
right = int(width - roi_data['ex'] * width) if 'ex' in roi_data else default_right
|
|
bottom = int(height - roi_data['ey'] * height) if 'ey' in roi_data else default_bottom
|
|
inner_width = width - left - right
|
|
inner_height = height - top - bottom
|
|
return (left, top, right, bottom, inner_width, inner_height)
|
|
|
|
|
|
class FlipStreamImageSize:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"tensor": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE","INT","INT","INT")
|
|
RETURN_NAMES = ("tensor","width","height","batchsize")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, tensor):
|
|
(batchsize, height, width) = tensor.shape[0:3]
|
|
return (tensor, width, height, batchsize)
|
|
|
|
|
|
class FlipStreamTextReplace:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {"default": "", "multiline": True}),
|
|
"find": ("STRING", {"default": ""}),
|
|
"replace": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"value": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, text, find, replace, value=None):
|
|
for word in find.split(","):
|
|
word = word.strip()
|
|
if not word:
|
|
continue
|
|
text = text.replace(word, replace.replace("{}", str(value)) if value is not None else replace)
|
|
return (text,)
|
|
|
|
|
|
class FlipStreamTextConcat:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"joinstr": ("STRING", {"default": "", "multiline": True}),
|
|
"enable": ("BOOLEAN", {"default": True})
|
|
},
|
|
"optional": {
|
|
**{f"text{i}": ("STRING", {"default": ""}) for i in range(20)}
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, joinstr, enable, **kwargs):
|
|
if not enable:
|
|
return ("",)
|
|
|
|
text = []
|
|
for k in kwargs:
|
|
if k.startswith("text"):
|
|
val = kwargs[k]
|
|
text.append(val)
|
|
|
|
return (str(joinstr).join(map(str, filter(None, text))),)
|
|
|
|
|
|
class FlipStreamGetFrame:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"index": ("INT", {"default": 0, "min": 0}),
|
|
"frames": ("INT", {"default": 1, "min": 1}),
|
|
"capture_only": ("BOOLEAN", {"default": True}),
|
|
"enable": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "BOOLEAN")
|
|
RETURN_NAMES = ("image", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, capture_only, **_):
|
|
if capture_only:
|
|
return setframe_mtime
|
|
else:
|
|
return frame_mtime
|
|
|
|
def run(self, index, frames, capture_only, enable, **_):
|
|
if not enable:
|
|
return (None, False)
|
|
buf = setframe_buffer if capture_only else frame_buffer
|
|
images_list = [
|
|
np.array(Image.open(io.BytesIO(buf[i])))
|
|
for i in range(index, min(index + frames, len(buf)))
|
|
]
|
|
if not images_list:
|
|
return (None, False)
|
|
image = torch.from_numpy(np.stack(images_list)).float() / 255.0
|
|
if image is not None and image.shape[3] == 4:
|
|
image = image[:,:,:,:3] * image[:,:,:,3:4]
|
|
return (image, True)
|
|
|
|
|
|
class FlipStreamScreenGrabber:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"left": ("INT", {"default": 0, "min": 0, "step": 32}),
|
|
"top": ("INT", {"default": 0, "min": 0, "step": 32}),
|
|
"width": ("INT", {"default": 512, "min": 256, "max": 2048, "step": 32}),
|
|
"height": ("INT", {"default": 512, "min": 256, "max": 2048, "step": 32}),
|
|
"frames": ("INT", {"default": 8, "min": 1, "max": 100}),
|
|
"fps": ("INT", {"default": 8, "min": 1, "max": 30}),
|
|
"delay_sec": ("FLOAT", {"default": 3, "min": 0.0, "max": 30.0, "step": 0.1}),
|
|
"enable": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "BOOLEAN")
|
|
RETURN_NAMES = ("image", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def __init__(self):
|
|
self.grabber_thread = None
|
|
self.flag_stop_grabber = False
|
|
self.last_args = None
|
|
self.grabbed_frames = []
|
|
|
|
def grabber(self, area, frames, fps):
|
|
with mss.mss() as sct:
|
|
while True:
|
|
self.grabbed_frames.append(sct.grab(area)) # BGRA format
|
|
self.grabbed_frames = self.grabbed_frames[-frames:]
|
|
time.sleep(1 / fps)
|
|
if self.flag_stop_grabber:
|
|
break
|
|
|
|
def start_grabber(self, area, frames, fps):
|
|
if self.grabber_thread is None or not self.grabber_thread.is_alive():
|
|
self.flag_stop_grabber = False
|
|
self.grabber_thread = threading.Thread(target=self.grabber, args=(area, frames, fps), daemon=True)
|
|
self.grabber_thread.start()
|
|
|
|
def stop_grabber(self):
|
|
if self.grabber_thread is not None and self.grabber_thread.is_alive():
|
|
self.flag_stop_grabber = True
|
|
self.grabber_thread.join()
|
|
|
|
def __del__(self):
|
|
self.stop_grabber()
|
|
|
|
def run(self, top, left, width, height, frames, fps, delay_sec, enable):
|
|
image = None
|
|
if enable:
|
|
enable = False
|
|
area = {"left": left, "top": top, "width": width, "height": height}
|
|
if self.last_args is None or self.last_args != (area, frames, fps):
|
|
self.last_args = (area, frames, fps)
|
|
self.stop_grabber()
|
|
self.start_grabber(area, frames, fps)
|
|
time.sleep(delay_sec)
|
|
buf = self.grabbed_frames[-frames:]
|
|
if len(buf) == frames:
|
|
image = torch.tensor(np.array(buf)[:, :, :, (2, 1, 0)] / 255, dtype=torch.float32)
|
|
enable = True
|
|
else:
|
|
self.stop_grabber()
|
|
return (image, enable)
|
|
|
|
|
|
class FlipStreamVideoInput:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"path": ("STRING", {"default": ""}),
|
|
"first": ("INT", {"default": 0, "min": 0}),
|
|
"step": ("INT", {"default": 1, "min": 1}),
|
|
"frames": ("INT", {"default": 1, "min": 1}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "BOOLEAN")
|
|
RETURN_NAMES = ("image", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, path, first, step, frames):
|
|
if not path or not Path(path).is_file():
|
|
return (torch.zeros((1, 64, 64, 3)), False)
|
|
with iio.imopen(path, "r") as file:
|
|
buf = [file.read(index=i) for i in range(first, first+(frames-1)*step+1, step)]
|
|
buf = np.stack(buf).astype(np.float32) / 255
|
|
enable = buf.shape[0] == frames
|
|
image = torch.from_numpy(buf) if enable else None
|
|
return (image, enable)
|
|
|
|
|
|
class FlipStreamSource:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"width": ("INT", {"default": 0, "min": 0, "max": 2048, "step": 32}),
|
|
"height": ("INT", {"default": 0, "min": 0, "max": 2048, "step": 32}),
|
|
"frames": ("INT", {"default": 0, "min": 0}),
|
|
"strim": ("INT", {"default": 0, "min": 0}),
|
|
"etrim": ("INT", {"default": 0, "min": 0}),
|
|
},
|
|
"optional": {
|
|
"image": ("IMAGE",),
|
|
"vae": ("VAE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "LATENT", "BOOLEAN")
|
|
RETURN_NAMES = ("image", "latent", "enable")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, width, height, frames, strim, etrim, image=None, vae=None):
|
|
latent = None
|
|
if image is not None and not torch.any(image):
|
|
image = None
|
|
if image is not None:
|
|
if image.shape[3] == 4:
|
|
image = image[:,:,:,:3] * image[:,:,:,3:4]
|
|
if height and not width:
|
|
width = int(image.shape[2] * height / image.shape[1] // 32 * 32)
|
|
if width and not height:
|
|
height = int(image.shape[1] * width / image.shape[2] // 32 * 32)
|
|
if not width and not height:
|
|
width = image.shape[2]
|
|
height = image.shape[1]
|
|
if not frames:
|
|
frames = image.shape[0]
|
|
if image is not None and image.shape[0] >= frames and frames > strim + etrim:
|
|
buf = image[strim:frames-etrim]
|
|
buf = buf.movedim(-1,1)
|
|
buf = comfy.utils.common_upscale(buf, width, height, "lanczos", "center")
|
|
image = buf.movedim(1,-1)
|
|
if vae:
|
|
latent = {"samples": vae.encode(image)}
|
|
else:
|
|
latent = {"samples": torch.zeros([frames, 4, height // 8, width // 8], device=comfy.model_management.intermediate_device())}
|
|
return (image, latent, True)
|
|
else:
|
|
if not frames:
|
|
frames = 1
|
|
if not width:
|
|
width = 64
|
|
if not height:
|
|
height = 64
|
|
image = torch.zeros([frames, height, width, 3])
|
|
latent = {"samples": torch.zeros([frames, 4, height // 8, width // 8], device=comfy.model_management.intermediate_device())}
|
|
return (image, latent, False)
|
|
|
|
|
|
class FlipStreamSwitch:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"value": (anytype,),
|
|
},
|
|
"optional": {
|
|
"value_enable": (anytype,),
|
|
"enable": ("BOOLEAN",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, value=None, value_enable=None, enable=None):
|
|
if enable:
|
|
return (value_enable,)
|
|
return (value,)
|
|
|
|
|
|
class FlipStreamSwitchImage:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
},
|
|
"optional": {
|
|
"image_enable": ("IMAGE",),
|
|
"enable": ("BOOLEAN",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, image=None, image_enable=None, enable=None):
|
|
if enable:
|
|
return (image_enable,)
|
|
return (image,)
|
|
|
|
|
|
class FlipStreamSwitchLatent:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"latent": ("LATENT",),
|
|
},
|
|
"optional": {
|
|
"latent_enable": ("LATENT",),
|
|
"enable": ("BOOLEAN",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, latent, latent_enable=None, enable=None):
|
|
if enable:
|
|
return (latent_enable,)
|
|
return (latent,)
|
|
|
|
|
|
class FlipStreamGate:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL",),
|
|
"pos": ("CONDITIONING",),
|
|
"neg": ("CONDITIONING",),
|
|
"latent": ("LATENT",),
|
|
},
|
|
"optional": {
|
|
"a": (anytype,),
|
|
"b": (anytype,),
|
|
"c": (anytype,),
|
|
"d": (anytype,),
|
|
"e": (anytype,),
|
|
"f": (anytype,),
|
|
"g": (anytype,),
|
|
"h": (anytype,)
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", anytype, anytype, anytype, anytype, anytype, anytype, anytype, anytype)
|
|
RETURN_NAMES = ("model", "pos", "neg", "latent", "a", "b", "c", "d", "e", "f", "g", "h")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, model, pos, neg, latent, a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None):
|
|
return (model, pos, neg, latent, a, b, c, d, e, f, g, h)
|
|
|
|
|
|
class FlipStreamRembg:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
},
|
|
"optional": {
|
|
"enable": ("BOOLEAN", {"default": True}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK")
|
|
FUNCTION = "run"
|
|
CATEGORY = "image"
|
|
|
|
def __init__(self):
|
|
self.rembg = rembg.InspyrenetRembg() if rembg is not None else None
|
|
|
|
def run(self, image, enable=True):
|
|
if not enable:
|
|
return (image, torch.ones(image.shape[:3], dtype=image.dtype, device=image.device))
|
|
if self.rembg is None:
|
|
raise RuntimeError("FlipStreamRembg: ComfyUI-Inspyrenet-Rembg must be installed to use this function.")
|
|
img, mask = self.rembg.remove_background(image, "default")
|
|
return (img[..., :3] * img[..., 3:4], mask)
|
|
|
|
|
|
class FlipStreamSegMask:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"tensor": ("IMAGE",),
|
|
"target": ("STRING", {"default": "", "multiline": True}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK",)
|
|
RETURN_NAMES =("preview", "mask",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
model = None
|
|
processor = None
|
|
|
|
def run(self, tensor, target):
|
|
if florence2 is None:
|
|
raise RuntimeError("FlipStreamSegMask: ComfyUI-Florence2 must be installed to use this function.")
|
|
|
|
# Create mask
|
|
_, h, w = tensor.shape[0:3]
|
|
mask_image = Image.new('RGB', (w, h), 'black')
|
|
|
|
# Skip if empty target
|
|
if target.strip() == "":
|
|
mask_tensor = torch.from_numpy(np.array(mask_image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
return (mask_tensor, mask_tensor[:,:,:,0])
|
|
|
|
# Download model if it not found
|
|
model_id = 'microsoft/Florence-2-large'
|
|
if FlipStreamSegMask.model is None:
|
|
model_dir = Path(folder_paths.models_dir, "LLM")
|
|
model_dir.mkdir(exist_ok=True)
|
|
model_name = model_id.rsplit('/', 1)[-1]
|
|
model_path = Path(model_dir, model_name)
|
|
|
|
if not model_path.exists():
|
|
print(f"Downloading Florence2 model to: {model_path}")
|
|
from huggingface_hub import snapshot_download
|
|
snapshot_download(repo_id=model_id, local_dir=str(model_path), local_dir_use_symlinks=False)
|
|
|
|
# Load model
|
|
if FlipStreamSegMask.model is None:
|
|
FlipStreamSegMask.model = transformers.Florence2ForConditionalGeneration.from_pretrained(
|
|
model_id,
|
|
trust_remote_code=True,
|
|
attn_implementation='sdpa',
|
|
device_map=comfy.model_management.get_torch_device(),
|
|
torch_dtype=torch.bfloat16
|
|
)
|
|
FlipStreamSegMask.processor = transformers.AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
|
|
model = FlipStreamSegMask.model
|
|
processor = FlipStreamSegMask.processor
|
|
model.to(comfy.model_management.get_torch_device())
|
|
|
|
# Process prompt for each target
|
|
task_prompt = '<REFERRING_EXPRESSION_SEGMENTATION>'
|
|
image = Image.fromarray(np.clip(255. * tensor[0,:,:,:].cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
|
mask_draw = ImageDraw.Draw(mask_image)
|
|
for item in target.split(','):
|
|
prompt = task_prompt + item
|
|
inputs = processor(text=prompt, images=image, return_tensors="pt", do_rescale=False).to('cuda', torch.bfloat16)
|
|
generated_ids = model.generate(
|
|
input_ids=inputs["input_ids"].cuda(),
|
|
pixel_values=inputs["pixel_values"].cuda(),
|
|
max_new_tokens=1024,
|
|
early_stopping=False,
|
|
do_sample=False,
|
|
num_beams=1,
|
|
)
|
|
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
|
|
parsed_answer = processor.post_process_generation(generated_text, task=task_prompt, image_size=(w, h))
|
|
|
|
# Draw mask
|
|
predictions = parsed_answer[task_prompt]
|
|
for polygons in predictions['polygons']:
|
|
for p in polygons:
|
|
p = np.array(p).reshape(-1, 2)
|
|
p = np.clip(p, [0, 0], [w - 1, h - 1])
|
|
if len(p) < 3:
|
|
print('Invalid polygon:', p)
|
|
continue
|
|
|
|
p = p.reshape(-1).tolist()
|
|
mask_draw.polygon(p, outline="white", fill="white")
|
|
|
|
# Offload model
|
|
model.to(torch.device("cpu"))
|
|
|
|
mask_tensor = torch.from_numpy(np.array(mask_image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
return (mask_tensor, mask_tensor[:,:,:,0])
|
|
|
|
|
|
class FlipStreamChat:
|
|
CACHED_OUTPUT = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model_file": ([path.name for path in Path(folder_paths.models_dir, "LLM").glob("*.gguf")],),
|
|
"n_ctx": ("INT", {"default": 2048, "min": 0, "max": 8192}),
|
|
"n_gpu_layers": ("INT", {"default": -1, "min": -1}),
|
|
"unload_other_models": ("BOOLEAN", {"default": False}),
|
|
"close_after_use": ("BOOLEAN", {"default": False}),
|
|
"system": ("STRING", {"default": "", "multiline": True}),
|
|
"user": ("STRING", {"default": "", "multiline": True}),
|
|
"instant": ("BOOLEAN", {"default": False}),
|
|
"max_history": ("INT", {"default": 10, "min": -1}),
|
|
"stop": ("STRING", {"default": "[,<"}),
|
|
"temperature": ("FLOAT", {"default": 0.2, "min": 0}),
|
|
"top_p": ("FLOAT", {"default": 0.95, "min": 0}),
|
|
"seed": ("INT", {"default": -1}),
|
|
"max_tokens": ("INT", {"default": 1024, "min": 0, "max": 8192}),
|
|
"presence_penalty": ("FLOAT", {"default": 0, "min": 0}),
|
|
"frequency_penalty": ("FLOAT", {"default": 0.5, "min": 0}),
|
|
"repeat_penalty": ("FLOAT", {"default": 1.0, "min": 0}),
|
|
"response_format": ("STRING", {"default": "", "multiline": True}),
|
|
"enable": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"chat_model": ("CHAT_MODEL",),
|
|
"messages": ("MESSAGES",)
|
|
},
|
|
"hidden": {
|
|
"unique_id": "UNIQUE_ID"
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("CHAT_MODEL", "STRING", "MESSAGES")
|
|
RETURN_NAMES = ("chat_model", "response", "messages")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def __init__(self):
|
|
self.model = None
|
|
self.system = None
|
|
self.messages = []
|
|
|
|
def _unload_other_models(self):
|
|
try:
|
|
comfy.model_management.unload_all_models()
|
|
comfy.model_management.soft_empty_cache(True)
|
|
comfy.gc.collect()
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.ipc_collect()
|
|
except:
|
|
pass
|
|
|
|
def load_model(self, model_file, n_ctx, n_gpu_layers, unload_other_models):
|
|
h = hash((model_file, n_ctx, n_gpu_layers))
|
|
if self.model is None or self.model._FlipStreamChat_is_closed or self.model._FlipStreamChat_last_hash != h:
|
|
model_path = Path(folder_paths.models_dir, "LLM", model_file)
|
|
if not model_path.exists():
|
|
raise RuntimeError(f"FlipStreamChat: {model_path} not found.")
|
|
if Llama is None:
|
|
raise RuntimeError("FlipStreamChat: llama-cpp-python required.")
|
|
if unload_other_models:
|
|
self._unload_other_models()
|
|
try:
|
|
self.model = Llama(str(model_path), chat_format=None, n_ctx=n_ctx, n_gpu_layers=n_gpu_layers, flash_attn=True, verbose=False)
|
|
except:
|
|
# Sometimes need retry when using ComfyUI v0.14.1
|
|
self._unload_other_models()
|
|
self.model = Llama(str(model_path), chat_format=None, n_ctx=n_ctx, n_gpu_layers=n_gpu_layers, flash_attn=True, verbose=False)
|
|
self.model._FlipStreamChat_is_closed = False
|
|
self.model._FlipStreamChat_last_hash = h
|
|
if LlamaRAMCache is not None:
|
|
self.model.set_cache(LlamaRAMCache())
|
|
|
|
def close_model(self):
|
|
if self.model:
|
|
self.model.close()
|
|
self.model._FlipStreamChat_is_closed = True
|
|
|
|
def chat(self, system, user, stop, messages, response_format, **kwargs):
|
|
if system != self.system:
|
|
self.system = system
|
|
messages.clear()
|
|
self.model.reset()
|
|
if system and len(messages) == 0:
|
|
messages.append(dict(role="system", content=system))
|
|
if system and messages[0]["role"] != "system":
|
|
messages[0] = dict(role="system", content=system)
|
|
if user:
|
|
messages.append(dict(role="user", content=user))
|
|
if response_format:
|
|
response_format = json.loads(response_format)
|
|
else:
|
|
response_format = None
|
|
return self.model.create_chat_completion(messages, stop=list(filter(str.strip, stop.split(","))), response_format=response_format, **kwargs)["choices"][0]["message"]
|
|
|
|
def run(self, model_file, n_ctx, n_gpu_layers, unload_other_models, close_after_use, system, user, instant, max_history, stop, enable, unique_id, chat_model=None, messages=None, **kwargs):
|
|
if chat_model is not None:
|
|
self.model = chat_model
|
|
if messages is None:
|
|
messages = self.messages
|
|
if not enable:
|
|
if close_after_use:
|
|
self.close_model()
|
|
return (self.model, None, messages)
|
|
|
|
if max_history == 0:
|
|
messages.clear()
|
|
elif max_history >= 1:
|
|
messages[:] = messages[:max_history]
|
|
self.load_model(model_file, n_ctx, n_gpu_layers, unload_other_models)
|
|
cache = FlipStreamChat.CACHED_OUTPUT
|
|
if instant:
|
|
res = self.chat(system, user, stop, messages.copy(), **kwargs)
|
|
cache[unique_id] = res["content"]
|
|
else:
|
|
res = self.chat(system, user, stop, messages, **kwargs)
|
|
cache[unique_id] = res["content"]
|
|
if res["role"] == "assistant":
|
|
messages.append(dict(role="assistant", content=cache[unique_id]))
|
|
self.messages = messages
|
|
if close_after_use:
|
|
self.close_model()
|
|
return (self.model, cache.get(unique_id), messages)
|
|
|
|
|
|
class FlipStreamParseJson:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"json_input": ("STRING", {"default": "", "multiline": True}),
|
|
"keys": ("STRING", {"default": "", "multiline": True}),
|
|
"joinstr": ("STRING", {"default": ",", "multiline": True}),
|
|
"ignore_error": ("BOOLEAN", {"default": True}),
|
|
"enable": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, json_input, keys, joinstr, ignore_error, enable):
|
|
if not enable:
|
|
return ("",)
|
|
|
|
value = []
|
|
try:
|
|
for key in keys.split("\n"):
|
|
value.append(json.loads(json_input.replace('\n', ' '), strict=False)[key.strip()])
|
|
except Exception as e:
|
|
if not ignore_error:
|
|
raise RuntimeError(f"FlipStreamParseJsonItem: Invalid JSON input: {e}: {json_input}")
|
|
return (joinstr.join(value),)
|
|
|
|
|
|
class FlipStreamChatJson(FlipStreamChat):
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model_file": ([path.name for path in Path(folder_paths.models_dir, "LLM").glob("*.gguf")],),
|
|
"unload_other_models": ("BOOLEAN", {"default": False}),
|
|
"close_after_use": ("BOOLEAN", {"default": False}),
|
|
"system": ("STRING", {"default": "", "multiline": True}),
|
|
"user": ("STRING", {"default": "", "multiline": True}),
|
|
"seed": ("INT", {"default": -1}),
|
|
"n_ctx": ("INT", {"default": 2048, "min": 0, "max": 8192}),
|
|
"temperature": ("FLOAT", {"default": 0.2, "min": 0}),
|
|
"top_p": ("FLOAT", {"default": 0.95, "min": 0}),
|
|
"enable": ("BOOLEAN", {"default": True})
|
|
},
|
|
"optional": {
|
|
"chat_model": ("CHAT_MODEL",),
|
|
**{f"label{i}": ("STRING", {"default": ""}) for i in range(20)}
|
|
},
|
|
"hidden": {
|
|
"unique_id": "UNIQUE_ID"
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("CHAT_MODEL", "STRING", "MESSAGES") + (anytype,) * 20
|
|
RETURN_NAMES = ("chat_model", "response", "messages") + tuple(f"value{i}" for i in range(20))
|
|
FUNCTION = "run2"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run2(self, **kwargs):
|
|
kwargs.update({
|
|
"n_gpu_layers": -1,
|
|
"instant": False,
|
|
"max_history": 0,
|
|
"stop": "",
|
|
"max_tokens": kwargs["n_ctx"] - 512,
|
|
"presence_penalty": 0,
|
|
"frequency_penalty": 0.5,
|
|
"repeat_penalty": 1.0
|
|
})
|
|
|
|
label = {}
|
|
for k in list(kwargs):
|
|
if k.startswith('label'):
|
|
if kwargs[k]:
|
|
label[k] = re.sub(r'["\\{}\r\n]', '', kwargs[k]).strip()
|
|
del kwargs[k]
|
|
|
|
response_format = {
|
|
"type": "json_object",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {k: {"type": "string"} for k in label.values()},
|
|
"required": list(label.values())
|
|
}
|
|
}
|
|
kwargs["response_format"] = json.dumps(response_format, ensure_ascii=False, indent=4)
|
|
chat_model, response, messages = self.run(**kwargs)
|
|
value = {f"label{i}": "" for i in range(20)}
|
|
if response:
|
|
data = json.loads(response.replace('\n', ' '), strict=False)
|
|
value.update({k: str(data.get(v, "")) for k, v in label.items()})
|
|
return (chat_model, response, messages, *value.values())
|
|
|
|
|
|
class FlipStreamBatchPrompt:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"prompt": ("STRING", {"default": "", "multiline": True}),
|
|
"clip": ("CLIP",),
|
|
"frames": ("INT",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("CONDITIONING",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, prompt, clip, frames):
|
|
cond_buf = []
|
|
pooled_buf = []
|
|
|
|
buf = prompt.split("----")
|
|
prompt = buf[0].strip()
|
|
batchPrompt = buf[1].strip() if len(buf) > 1 else ""
|
|
appPrompt = buf[2].strip() if len(buf) > 2 else ""
|
|
count = len(batchPrompt.split("\n"))
|
|
batchPrompt = ",\n".join([f'"{int(frames * n / count)}":"{item.lstrip("-").strip()}"' for n, item in enumerate(batchPrompt.split("\n"))])
|
|
|
|
tokens = clip.tokenize(prompt)
|
|
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
|
|
s = json.loads("{" + batchPrompt + "}")
|
|
for i in range(frames):
|
|
if str(i) in s:
|
|
tokens = clip.tokenize(" ".join([prompt, s[str(i)], appPrompt]).strip())
|
|
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
|
|
cond_buf.append(cond)
|
|
pooled_buf.append(pooled)
|
|
|
|
max_length = max([t.size(1) for t in cond_buf])
|
|
cond_buf = [
|
|
NNF.pad(t, (0, 0, 0, max_length - t.size(1))) if t.size(1) < max_length else t[:, :max_length, :]
|
|
for t in cond_buf
|
|
]
|
|
cond = torch.cat(cond_buf, dim=0)
|
|
if any(p is None for p in pooled_buf) or pooled_buf[0].ndim < 2:
|
|
pooled = torch.zeros((frames, cond.size(-1)), device=cond.device, dtype=cond.dtype)
|
|
else:
|
|
pooled = torch.cat(pooled_buf, dim=0)
|
|
return ([[cond, {"pooled_output": pooled}]],)
|
|
|
|
|
|
class FlipStreamFilmVfi:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"frames": ("IMAGE",),
|
|
"multiplier": ("INT", {"default": 1, "min": 1, "max": 16}),
|
|
"clear_cache_after_n_frames": ("INT", {"default": 10, "min": 1, "max": 1000}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
model = None
|
|
|
|
def run(self, frames, multiplier, clear_cache_after_n_frames):
|
|
if frames.shape[0] < 2 or multiplier < 2:
|
|
return (frames,)
|
|
|
|
if film is None:
|
|
raise RuntimeError("FlipStreamFilmVfi: ComfyUI-Frame-Interpolation must be installed to use this function.")
|
|
|
|
if FlipStreamFilmVfi.model is None:
|
|
model_path = film.load_file_from_github_release("film", "film_net_fp32.pt")
|
|
model = torch.jit.load(model_path, map_location="cpu")
|
|
model.eval()
|
|
FlipStreamFilmVfi.model = model
|
|
model = FlipStreamFilmVfi.model
|
|
model = model.to(comfy.model_management.get_torch_device())
|
|
dtype = torch.float32
|
|
|
|
frames = film.preprocess_frames(frames)
|
|
number_of_frames_processed_since_last_cleared_cuda_cache = 0
|
|
output_frames = []
|
|
|
|
for frame_itr in range(len(frames) - 1):
|
|
frame_0 = frames[frame_itr:frame_itr+1].to(comfy.model_management.get_torch_device()).float()
|
|
frame_1 = frames[frame_itr+1:frame_itr+2].to(comfy.model_management.get_torch_device()).float()
|
|
relust = film.inference(model, frame_0, frame_1, multiplier - 1)
|
|
output_frames.extend([frame.detach().cpu().to(dtype=dtype) for frame in relust[:-1]])
|
|
|
|
number_of_frames_processed_since_last_cleared_cuda_cache += 1
|
|
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
|
|
film.soft_empty_cache()
|
|
number_of_frames_processed_since_last_cleared_cuda_cache = 0
|
|
|
|
output_frames.append(frames[-1:].to(dtype=dtype))
|
|
output_frames = [frame.cpu() for frame in output_frames]
|
|
out = torch.cat(output_frames, dim=0)
|
|
film.soft_empty_cache()
|
|
model.to(torch.device("cpu"))
|
|
return (film.postprocess_frames(out),)
|
|
|
|
|
|
class FlipStreamViewer:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"tensor": ("IMAGE",),
|
|
"allowip": ("STRING", {"default": ""}),
|
|
"idle": ("FLOAT", {"default": 1.0, "min": 0.0}),
|
|
"fps": ("INT", {"default": 16, "min": 1, "max": 30}),
|
|
"loramode": ("STRING", {"default": ""}),
|
|
"pingpong": ("BOOLEAN", {"default": True}),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, allowip, idle, loramode, **_):
|
|
global allowed_ips
|
|
allowed_ips = ["127.0.0.1"] + list(map(str.strip, allowip.split(",")))
|
|
state["loraMode"] = loramode
|
|
time.sleep(idle)
|
|
return None
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, allowip, tensor, fps, loramode, pingpong, **_):
|
|
global allowed_ips
|
|
allowed_ips = ["127.0.0.1"] + list(map(str.strip, allowip.split(",")))
|
|
state["loraMode"] = loramode
|
|
|
|
fb = []
|
|
buf = (tensor.detach().cpu().numpy() * 255).astype(np.uint8)
|
|
if tensor.shape[0] != 1 and pingpong:
|
|
buf = np.concatenate([buf, np.flip(buf, axis=0)])
|
|
for image in buf:
|
|
with io.BytesIO() as output:
|
|
img = Image.fromarray(image)
|
|
img.save(output, format="PNG", compress_level=STREAM_COMPRESSION)
|
|
fb.append(output.getvalue())
|
|
global frame_buffer
|
|
global frame_mtime
|
|
global frame_fps
|
|
frame_buffer = fb
|
|
frame_mtime = time.time()
|
|
frame_fps = fps
|
|
return ()
|
|
|
|
|
|
class FlipStreamViewerSimple:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"tensor": ("IMAGE",),
|
|
"idle": ("FLOAT", {"default": 0, "min": 0.0}),
|
|
"fps": ("INT", {"default": 16, "min": 1, "max": 30}),
|
|
"pingpong": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, idle, **_):
|
|
time.sleep(idle)
|
|
return None
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
RETURN_NAMES = ("hook",)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, tensor, fps, pingpong, hook=True, **_):
|
|
fb = []
|
|
buf = (tensor.detach().cpu().numpy() * 255).astype(np.uint8)
|
|
if tensor.shape[0] != 1 and pingpong:
|
|
buf = np.concatenate([buf, np.flip(buf, axis=0)])
|
|
for image in buf:
|
|
with io.BytesIO() as output:
|
|
img = Image.fromarray(image)
|
|
img.save(output, format="PNG", compress_level=STREAM_COMPRESSION)
|
|
fb.append(output.getvalue())
|
|
global frame_buffer
|
|
global frame_mtime
|
|
global frame_fps
|
|
frame_buffer = fb
|
|
frame_mtime = time.time()
|
|
frame_fps = fps
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamViewerVideo:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"idle": ("FLOAT", {"default": 0, "min": 0.0}),
|
|
"fps": ("INT", {"default": 24, "min": 1, "max": 60}),
|
|
"crf": ("INT", {"default": 18, "min": 0, "max": 51}),
|
|
"preset": (["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"], {"default": "superfast"}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
"tensor": ("IMAGE",),
|
|
"audio": ("AUDIO",),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, idle, **_):
|
|
time.sleep(idle)
|
|
return None
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
RETURN_NAMES = ("hook",)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, fps, crf, preset, hook=True, tensor=None, audio=None, **_):
|
|
if tensor is None:
|
|
return (hook,)
|
|
buf = (tensor.detach().cpu().numpy() * 255).astype(np.uint8)
|
|
video = encode_video_mp4(buf, fps, crf, preset, audio)
|
|
global frame_buffer
|
|
global frame_mtime
|
|
global frame_fps
|
|
global stream_video
|
|
global stream_video_mtime
|
|
global stream_video_audio
|
|
frame_buffer = []
|
|
frame_mtime = time.time()
|
|
frame_fps = fps
|
|
stream_video = video
|
|
stream_video_mtime = frame_mtime
|
|
stream_video_audio = audio is not None
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamLogger:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"label": ("STRING", {"default": "log"}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
"data": (anytype,),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
RETURN_NAMES = ("hook",)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, label, hook=True, data=None, **_):
|
|
if isinstance(data, str):
|
|
text = data
|
|
elif data is None:
|
|
text = "<None>"
|
|
else:
|
|
try:
|
|
text = json.dumps(data, ensure_ascii=False)
|
|
except Exception:
|
|
text = repr(data)
|
|
print(f"[Log:{label}] {text}", flush=True)
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamAllowIp:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"allowip": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, allowip, hook=True, **_):
|
|
global allowed_ips
|
|
allowed_ips = ["127.0.0.1"] + list(map(str.strip, allowip.split(",")))
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamCurrent:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"current": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, current, hook=True, **_):
|
|
state["current"] = current
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamLoraMode:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"loraMode": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, loraMode, hook=True, **_):
|
|
state["loraMode"] = loraMode
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamLoadLora:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL",),
|
|
"clip": ("CLIP",),
|
|
"mode": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL", "CLIP", "STRING")
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, mode, **_):
|
|
text = re.sub(rf"@(\w+)\{{(.*?)\}}", lambda m: m.group(2) if m.group(1) == mode else "", atob_utf8(param.get("lora", "")), flags=re.S)
|
|
return hash(text.strip())
|
|
|
|
def run(self, model, clip, mode, hook=True):
|
|
text = re.sub(rf"@(\w+)\{{(.*?)\}}", lambda m: m.group(2) if m.group(1) == mode else "", atob_utf8(param.get("lora", "")), flags=re.S)
|
|
|
|
def apply(m):
|
|
nonlocal model, clip
|
|
name, w = m.group(1), float(m.group(2))
|
|
for file in folder_paths.get_filename_list("loras"):
|
|
if Path(file).name.startswith(name):
|
|
if path := folder_paths.get_full_path("loras", file):
|
|
model, clip = comfy.sd.load_lora_for_models(model, clip, comfy.utils.load_torch_file(path), w, w)
|
|
print(f"FlipStreamLoadLora: loaded: {name} -> {path}")
|
|
break
|
|
else:
|
|
print(f"FlipStreamLoadLora: LoRA not found: {name}")
|
|
return ""
|
|
|
|
text = re.sub(r"\<lora:(.*?):(.*?)\>", apply, text).strip()
|
|
return (model, clip, text)
|
|
|
|
|
|
class FlipStreamSaveApiWorkflow:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"savepath": ("STRING", {"default": ""}),
|
|
"override": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT"
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, savepath, override, hook=True, prompt=None):
|
|
p = Path(savepath)
|
|
if prompt and (override or not p.exists()):
|
|
p.write_text(json.dumps(prompt))
|
|
print(f"FlipStreamSaveApiWorkflow: saved {savepath}")
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamRunApiWorkflow:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"path": ("STRING", {"default": ""}),
|
|
"enable": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, enable, path, hook=None):
|
|
if enable:
|
|
prompt = json.loads(Path(path).read_text())
|
|
print(f"FlipStreamRunApiWorkflow: queue {path}")
|
|
requests.post(f"http://127.0.0.1:{server.PromptServer.instance.port}/prompt", json={"prompt": prompt})
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamFree:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"unload_models": ("BOOLEAN", {"default": True}),
|
|
"free_memory": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, unload_models, free_memory, hook=None):
|
|
requests.post(f"http://127.0.0.1:{server.PromptServer.instance.port}/free", json={"unload_models": unload_models, "free_memory": free_memory})
|
|
return (hook,)
|
|
|
|
|
|
class FlipStreamShutdown:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"minutes": ("INT", {"default": 30, "min": 1}),
|
|
},
|
|
"optional": {
|
|
"hook": (anytype,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (anytype,)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "run"
|
|
CATEGORY = "FlipStreamViewer"
|
|
|
|
def run(self, minutes, hook=True, **_):
|
|
subprocess.run(["shutdown", "/a"], stderr=subprocess.DEVNULL)
|
|
subprocess.run(["shutdown", "/s", "/t", str(minutes * 60)])
|
|
return (hook,)
|
|
|
|
|
|
WEB_DIRECTORY = "./web"
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"FlipStreamSection": FlipStreamSection,
|
|
"FlipStreamButton": FlipStreamButton,
|
|
"FlipStreamSlider": FlipStreamSlider,
|
|
"FlipStreamTextBox": FlipStreamTextBox,
|
|
"FlipStreamInputBox": FlipStreamInputBox,
|
|
"FlipStreamSelectBox_Samplers": FlipStreamSelectBox_Samplers,
|
|
"FlipStreamSelectBox_Scheduler": FlipStreamSelectBox_Scheduler,
|
|
"FlipStreamSizeSelect": FlipStreamSizeSelect,
|
|
"FlipStreamGetSize": FlipStreamGetSize,
|
|
"FlipStreamFileSelect_Checkpoints": FlipStreamFileSelect_Checkpoints,
|
|
"FlipStreamFileSelect_DiffusionModels": FlipStreamFileSelect_DiffusionModels,
|
|
"FlipStreamFileSelect_Loras": FlipStreamFileSelect_Loras,
|
|
"FlipStreamFileSelect_VAE": FlipStreamFileSelect_VAE,
|
|
"FlipStreamFileSelect_LLM": FlipStreamFileSelect_LLM,
|
|
"FlipStreamFileSelect_ControlNetModel": FlipStreamFileSelect_ControlNetModel,
|
|
"FlipStreamFileSelect_TensorRT": FlipStreamFileSelect_TensorRT,
|
|
"FlipStreamFileSelect_AnimateDiffModel": FlipStreamFileSelect_AnimateDiffModel,
|
|
"FlipStreamFileSelect_Input": FlipStreamFileSelect_Input,
|
|
"FlipStreamFileSelect_Output": FlipStreamFileSelect_Output,
|
|
"FlipStreamPreviewBox": FlipStreamPreviewBox,
|
|
"FlipStreamPasteBox": FlipStreamPasteBox,
|
|
"FlipStreamLogBox": FlipStreamLogBox,
|
|
"FlipStreamRunOnce": FlipStreamRunOnce,
|
|
"FlipStreamSetMessage": FlipStreamSetMessage,
|
|
"FlipStreamAnd": FlipStreamAnd,
|
|
"FlipStreamOr": FlipStreamOr,
|
|
"FlipStreamSetState": FlipStreamSetState,
|
|
"FlipStreamGetState": FlipStreamGetState,
|
|
"FlipStreamSetParam": FlipStreamSetParam,
|
|
"FlipStreamGetParam": FlipStreamGetParam,
|
|
"FlipStreamGet": FlipStreamGet,
|
|
"FlipStreamGetFrame": FlipStreamGetFrame,
|
|
"FlipStreamGetPreviewRoi": FlipStreamGetPreviewRoi,
|
|
"FlipStreamImageSize": FlipStreamImageSize,
|
|
"FlipStreamTextReplace": FlipStreamTextReplace,
|
|
"FlipStreamTextConcat": FlipStreamTextConcat,
|
|
"FlipStreamScreenGrabber": FlipStreamScreenGrabber,
|
|
"FlipStreamVideoInput": FlipStreamVideoInput,
|
|
"FlipStreamSource": FlipStreamSource,
|
|
"FlipStreamSwitch": FlipStreamSwitch,
|
|
"FlipStreamSwitchImage": FlipStreamSwitchImage,
|
|
"FlipStreamSwitchLatent": FlipStreamSwitchLatent,
|
|
"FlipStreamGate": FlipStreamGate,
|
|
"FlipStreamRembg": FlipStreamRembg,
|
|
"FlipStreamSegMask": FlipStreamSegMask,
|
|
"FlipStreamChat": FlipStreamChat,
|
|
"FlipStreamParseJson": FlipStreamParseJson,
|
|
"FlipStreamChatJson": FlipStreamChatJson,
|
|
"FlipStreamBatchPrompt": FlipStreamBatchPrompt,
|
|
"FlipStreamFilmVfi": FlipStreamFilmVfi,
|
|
"FlipStreamViewer": FlipStreamViewer,
|
|
"FlipStreamViewerSimple": FlipStreamViewerSimple,
|
|
"FlipStreamViewerVideo": FlipStreamViewerVideo,
|
|
"FlipStreamLogger": FlipStreamLogger,
|
|
"FlipStreamAllowIp": FlipStreamAllowIp,
|
|
"FlipStreamCurrent": FlipStreamCurrent,
|
|
"FlipStreamLoraMode": FlipStreamLoraMode,
|
|
"FlipStreamLoadLora": FlipStreamLoadLora,
|
|
"FlipStreamSaveApiWorkflow": FlipStreamSaveApiWorkflow,
|
|
"FlipStreamRunApiWorkflow": FlipStreamRunApiWorkflow,
|
|
"FlipStreamFree": FlipStreamFree,
|
|
"FlipStreamShutdown": FlipStreamShutdown,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"FlipStreamSection": "FlipStreamSection",
|
|
"FlipStreamButton": "FlipStreamButton",
|
|
"FlipStreamSlider": "FlipStreamSlider",
|
|
"FlipStreamTextBox": "FlipStreamTextBox",
|
|
"FlipStreamInputBox": "FlipStreamInputBox",
|
|
"FlipStreamSelectBox_Samplers": "FlipStreamSelectBox_Samplers",
|
|
"FlipStreamSelectBox_Scheduler": "FlipStreamSelectBox_Scheduler",
|
|
"FlipStreamSizeSelect": "FlipStreamSizeSelect",
|
|
"FlipStreamGetSize": "FlipStreamGetSize",
|
|
"FlipStreamFileSelect_Checkpoints": "FlipStreamFileSelect_Checkpoints",
|
|
"FlipStreamFileSelect_DiffusionModels": "FlipStreamFileSelect_DiffusionModels",
|
|
"FlipStreamFileSelect_Loras": "FlipStreamFileSelect_Loras",
|
|
"FlipStreamFileSelect_VAE": "FlipStreamFileSelect_VAE",
|
|
"FlipStreamFileSelect_LLM": "FlipStreamFileSelect_LLM",
|
|
"FlipStreamFileSelect_ControlNetModel": "FlipStreamFileSelect_ControlNetModel",
|
|
"FlipStreamFileSelect_TensorRT": "FlipStreamFileSelect_TensorRT",
|
|
"FlipStreamFileSelect_AnimateDiffModel": "FlipStreamFileSelect_AnimateDiffModel",
|
|
"FlipStreamFileSelect_Input": "FlipStreamFileSelect_Input",
|
|
"FlipStreamFileSelect_Output": "FlipStreamFileSelect_Output",
|
|
"FlipStreamPreviewBox": "FlipStreamPreviewBox",
|
|
"FlipStreamPasteBox": "FlipStreamPasteBox",
|
|
"FlipStreamLogBox": "FlipStreamLogBox",
|
|
"FlipStreamRunOnce": "FlipStreamRunOnce",
|
|
"FlipStreamSetMessage": "FlipStreamSetMessage",
|
|
"FlipStreamAnd": "FlipStreamAnd(experimental)",
|
|
"FlipStreamOr": "FlipStreamOr(experimental)",
|
|
"FlipStreamSetState": "FlipStreamSetState",
|
|
"FlipStreamGetState": "FlipStreamGetState",
|
|
"FlipStreamSetParam": "FlipStreamSetParam",
|
|
"FlipStreamGetParam": "FlipStreamGetParam",
|
|
"FlipStreamGet": "FlipStreamGet(experimental)",
|
|
"FlipStreamGetFrame": "FlipStreamGetFrame",
|
|
"FlipStreamGetPreviewRoi": "FlipStreamGetPreviewRoi",
|
|
"FlipStreamImageSize": "FlipStreamImageSize",
|
|
"FlipStreamTextReplace": "FlipStreamTextReplace",
|
|
"FlipStreamTextConcat": "FlipStreamTextConcat",
|
|
"FlipStreamScreenGrabber": "FlipStreamScreenGrabber",
|
|
"FlipStreamVideoInput": "FlipStreamVideoInput",
|
|
"FlipStreamSource": "FlipStreamSource",
|
|
"FlipStreamSwitch": "FlipStreamSwitch",
|
|
"FlipStreamSwitchImage": "FlipStreamSwitchImage",
|
|
"FlipStreamSwitchLatent": "FlipStreamSwitchLatent",
|
|
"FlipStreamGate": "FlipStreamGate(deprecated)",
|
|
"FlipStreamRembg": "FlipStreamRembg",
|
|
"FlipStreamSegMask": "FlipStreamSegMask(deprecated)",
|
|
"FlipStreamChat": "FlipStreamChat",
|
|
"FlipStreamParseJson": "FlipStreamParseJson",
|
|
"FlipStreamChatJson": "FlipStreamChatJson(experimental)",
|
|
"FlipStreamBatchPrompt": "FlipStreamBatchPrompt",
|
|
"FlipStreamFilmVfi": "FlipStreamFilmVfi",
|
|
"FlipStreamViewer": "FlipStreamViewer",
|
|
"FlipStreamViewerSimple": "FlipStreamViewerSimple",
|
|
"FlipStreamViewerVideo": "FlipStreamViewerVideo",
|
|
"FlipStreamLogger": "FlipStreamLogger",
|
|
"FlipStreamAllowIp": "FlipStreamAllowIp",
|
|
"FlipStreamCurrent": "FlipStreamCurrent",
|
|
"FlipStreamLoraMode": "FlipStreamLoraMode",
|
|
"FlipStreamLoadLora": "FlipStreamLoadLora",
|
|
"FlipStreamSaveApiWorkflow": "FlipStreamSaveApiWorkflow",
|
|
"FlipStreamRunApiWorkflow": "FlipStreamRunApiWorkflow",
|
|
"FlipStreamFree": "FlipStreamFree",
|
|
"FlipStreamShutdown": "FlipStreamShutdown",
|
|
}
|