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: 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: 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 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() 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} frame_buffer = [] frame_mtime = 0 frame_fps = 16 setframe_mtime = 0 setframe_buffer = [] class AnyType(str): def __ne__(self, __value: object) -> bool: return False anytype = AnyType("*") HEAD=r""" FlipStreamViewer """ 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 = ''; 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]?", "g"); const match = re.exec(loraInput.value); if (match) { loraInput.setRangeText("", match.index, match.index + match[0].length, 'preserve'); } else { const loraTag = ``; 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; function detailsState() { const ds = document.querySelectorAll('details'); ds.forEach((d, i) => d.open = sessionStorage.getItem(`details_state-${i}`) === 'open'); 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 (streamViewFlag && streamCache.length > 0) { const img = streamCache[streamIndex]; container.style.backgroundImage = `url(${img.src})`; streamIndex = (streamIndex + 1) % streamCache.length; } else { container.style.backgroundImage = 'none'; } } 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("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}`; } }); const response = await fetch('/flipstreamviewer/stream/info'); streamInfo = await response.json(); if (streamInfo.mtime != streamMTime) { streamMTime = streamInfo.mtime; clearInterval(updateStreamInterval); streamIndex = 0; streamCache = await Promise.all( Array.from({ length: streamInfo.count }, (_, j) => new Promise(resolve => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => resolve(img); img.src = `/flipstreamviewer/stream/${j}.png?mtime=${streamInfo.mtime}`; }) ) ); updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps); } } } setInterval(refreshView, 1000); """ 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})`; 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("px")) { document.getElementById("presetXorkeyInputDiv").style.display = "block"; } if (p.has("darker")) { onInputDarker(p.get("darker")); } if (p.has("toggleView")) { toggleView(); } 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() return web.json_response({"mtime": frame_mtime, "fps": frame_fps, "count": len(frame_buffer)}) @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, **_): block[f"{title}_{section}"] = f"""
{section}""" def add_button(title, run, capture, **_): block[f"{title}"] = f"""
""" if run: block[f"{title}"] += f""" """ if capture: block[f"{title}"] += f""" """ block[f"{title}"] += f"""
""" 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"""
{float(param[label]):g}{label}
""" 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""" """ 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"""
{label}:
""" elif boxtype == "r4d": block[f"{title}_{label}"] = f"""
{label}:
""" else: block[f"{title}_{label}"] = f"""
{label}:
""" 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""" """ 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""" """ 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""" """ text_html += f"""
""" if use_move: text_html += f""" """ text_html += f"""
""" if use_move: text_html += f""" """ 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"""
""" 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"""
{label}
""" 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""" """ 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["_meta"]["title"] 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"""{HEAD}
Input {"".join([x[1] for x in sorted(block.items())])}
Status
{state["darker"]}drk
Preset
Lora
Rate:
X
Y
S
Status
Darker
{state["darker"]}drk
Preset
{"".join([f''' ''' for file in Path("preset", state["presetFolder"]).glob("*.json")])}
""" 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_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) status_info.append(f"{status_elapsed}s") if "current" in state: status_info.append(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: status_info.append(status["status_str"]) errinfo = status["messages"][2][1] 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_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/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() lorainfo["_lorapreview_href"] = "https://civitai.com/models/" + str(res["modelId"]) lorainfo["_lorapreview_src"] = res["images"][0]["url"] return web.json_response(lorainfo) @server.PromptServer.instance.routes.post("/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"}), }, "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), }, "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), } } @classmethod def INPUT_TYPES(s): return { "required": { "label": ("STRING", {"default": "empty"}), "default": (list(s.SIZEDICT["sdxl"].keys()),), "modeltype": (["sdxl", "qwen", "wan"],), "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) width, height = self.SIZEDICT[modeltype][param.get(label)] return (width, height, param[label] != "") 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 == "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_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) 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" 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.format(value)) 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",), }, } RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "run" CATEGORY = "image" def __init__(self): if rembg is None: raise RuntimeError("FlipStreamRembg: ComfyUI-Inspyrenet-Rembg must be installed to use this function.") self.rembg = rembg.InspyrenetRembg() def run(self, image): 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 = '' 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 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: 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 self.model = Llama(str(model_path), chat_format="llama-2", n_ctx=n_ctx, n_gpu_layers=n_gpu_layers, verbose=False) self.model._FlipStreamChat_is_closed = False self.model._FlipStreamChat_last_hash = h 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() 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, 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}), "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": "", "temperature":0.2, "top_p": 0.95, "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] = kwargs[k] 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, strict=False) value.update({k: data[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 ] return ([[torch.cat(cond_buf, dim=0), {"pooled_output":torch.cat(pooled_buf, dim=0)}]],) 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}), }, } @classmethod def IS_CHANGED(cls, idle, **_): time.sleep(idle) return None RETURN_TYPES = () OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "FlipStreamViewer" def run(self, tensor, fps, pingpong, **_): 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 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"\", 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_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, "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_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", "FlipStreamAllowIp": "FlipStreamAllowIp", "FlipStreamCurrent": "FlipStreamCurrent", "FlipStreamLoraMode": "FlipStreamLoraMode", "FlipStreamLoadLora": "FlipStreamLoadLora", "FlipStreamSaveApiWorkflow": "FlipStreamSaveApiWorkflow", "FlipStreamRunApiWorkflow": "FlipStreamRunApiWorkflow", "FlipStreamFree": "FlipStreamFree", "FlipStreamShutdown": "FlipStreamShutdown", }