From 0daa37e1c36a2e9f0016e03fe88e2b9f447b5aeb Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 8 Nov 2025 01:57:47 +0900 Subject: [PATCH 01/19] Update flipstreamviewer.py - Remove Tagger UI and WD14 functions. - Remove wd14exc option from FlipStreamViewer - Add FlipStreamButton - Add FlipStreamGetFrame, it can be use with WD14Tagger node. --- flipstreamviewer.py | 152 +++++++++++++++++++++++++------------------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index 404d347..dc715a5 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -31,11 +31,6 @@ try: except: Llama = None -try: - wd14tagger = __import__("comfyui-wd14-tagger").wd14tagger -except: - wd14tagger = None - try: rembg = __import__("comfyui-inspyrenet-rembg") except: @@ -69,14 +64,14 @@ UPDATE_DELAY = 1.0 allowed_ips = ["127.0.0.1"] setparam = {} setstate = {} +setframe_mtime = 0 default_param = {"lora": "", "_capture_offsetX": 0, "_capture_offsetY": 0, "_capture_scale": 100} param = default_param.copy() -state = {"message": "", "message_fontsize": "", "update_and_reload": False, "presetTitle": time.strftime("%Y%m%d-%H%M"), "presetFolder": "", "presetFile": "", "loraRate": "1", "loraRank": "0", "loraMode": "", "loraFolder": "", "loraFile": "", "loraTagOptions": "[]", "loraTag": "", "loraLinkHref": "", "loraPreviewSrc": "", "darker": 0.0, "wd14th": 0.35, "wd14cth": 0.85} +state = {"message": "", "message_fontsize": "", "update_and_reload": False, "presetTitle": time.strftime("%Y%m%d-%H%M"), "presetFolder": "", "presetFile": "", "loraRate": "1", "loraRank": "0", "loraMode": "", "loraFolder": "", "loraFile": "", "loraTagOptions": "[]", "loraTag": "", "loraLinkHref": "", "loraPreviewSrc": "", "darker": 0.0} frame_updating = None frame_buffer = [] frame_mtime = 0 frame_fps = 8 -exclude_tags = "" class AnyType(str): def __ne__(self, __value: object) -> bool: @@ -242,9 +237,7 @@ div.row { width: 80%; } -#darkerRange, -#wd14thRange, -#wd14cthRange { +#darkerRange { width: 50%; } @@ -314,9 +307,7 @@ function getStateAsJson(force_state={}) { const loraLinkHref = document.getElementById("loraLink").getAttribute("href"); const loraPreviewSrc = document.getElementById("loraPreview").getAttribute("src"); const darker = parseFloat(document.getElementById("darkerRange").value); - const wd14th = parseFloat(document.getElementById("wd14thRange").value); - const wd14cth = parseFloat(document.getElementById("wd14cthRange").value); - var res = { presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, loraRate: loraRate, loraRank: loraRank, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, darker: darker, wd14th: wd14th, wd14cth: wd14cth }; + var res = { presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, loraRate: loraRate, loraRank: loraRank, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, darker: darker }; document.querySelectorAll('.FlipStreamFolderSelect').forEach(x => res[x.name] = x.value); res = Object.assign(res, force_state); return res; @@ -667,17 +658,6 @@ function randomTag() { } } -async function addWD14Tag() { - const json = await (await fetch("/flipstreamviewer/get_wd14tag", { - method: "POST", - body: JSON.stringify(getStateAsJson()), - headers: {"Content-Type": "application/json"} - })).json(); - if (json.tags != "") { - loraInput.value = json.tags + "\n\n" + loraInput.value; - } -} - function clearLoraInput() { document.getElementById("loraInput").value = ""; } @@ -1055,6 +1035,18 @@ async def viewer(request): block[f"{title}_{section}"] = f"""
{section}
""" + def add_button(title, update, capture, hook): + block[f"{title}"] = f""" +
""" + if update: + 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.") @@ -1201,6 +1193,8 @@ async def viewer(request): 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": @@ -1219,9 +1213,6 @@ async def viewer(request): text_html = f"""{HEAD}
-
- -
{"".join([x[1] for x in sorted(block.items())])}
@@ -1229,25 +1220,12 @@ async def viewer(request):
Status
- +
Darker
{state["darker"]}drk
-
Tagger
-
- - -
-
- - {state["wd14th"]}wth -
-
- - {state["wd14cth"]}cth -
Preset
- +
""" elif boxtype == "r4d": block[f"{title}_{label}"] = f"""
{label}: - +
""" else: block[f"{title}_{label}"] = f"""
{label}: - +
""" def add_selectbox(title, label, default, listitems): @@ -1133,7 +1153,7 @@ async def viewer(request): 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)] + files = [Path(file) for file in FlipStreamFileSelect.get_filelist(folder_name, Path(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'] @@ -1213,20 +1233,25 @@ async def viewer(request): text_html = f"""{HEAD}
+
+ Input {"".join([x[1] for x in sorted(block.items())])} +
-
Status
+
+ Status -
Darker
{state["darker"]}drk
-
Preset
+
+
+ Preset {"".join([f'' for dir in Path("ComfyUI/models/loras", state["loraMode"]).glob("*/")])} @@ -1311,9 +1338,9 @@ async def viewer(request):
- + - +
@@ -1321,6 +1348,7 @@ async def viewer(request):
+
@@ -1419,16 +1447,13 @@ async def get_status(request): status_info.append(info["status_str"]) status_info += [errinfo[key] for key in ["node_id", "node_type", "exception_message", "exception_type"] if key in errinfo] - state.update(setstate) - setstate.clear() - data = {} + data = refresh_data.copy() + data["param"] = refresh_param.copy() + param.update(refresh_param) + refresh_param.clear() data["status_info"] = status_info data["preview_mtime"] = {key: state[key][0] for key in state if key.endswith("PreviewBox")} data["log"] = {key: state[key] for key in state if key.endswith("LogBox")} - data["message"] = state["message"] - data["message_fontsize"] = state["message_fontsize"] - data["update_and_reload"] = state["update_and_reload"] - state["update_and_reload"] = False return web.json_response(data) @@ -1439,11 +1464,7 @@ async def update_param(request): stt, prm = await request.json() state.update(stt) - state.update(setstate) - setstate.clear() param.update(prm) - param.update(setparam) - setparam.clear() time.sleep(UPDATE_DELAY) return web.Response() @@ -1488,8 +1509,10 @@ async def set_frame(request): 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() @@ -1803,6 +1826,11 @@ class FlipStreamFileSelect_Checkpoints(FlipStreamFileSelect): 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() @@ -1855,6 +1883,8 @@ class FlipStreamPreviewBox: CATEGORY = "FlipStreamViewer" def run(self, label, tensor, **kwargs): + 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)) @@ -1904,7 +1934,7 @@ class FlipStreamSetUpdateAndReload: def run(self, delay_sec, hook=None): time.sleep(delay_sec) - setstate["update_and_reload"] = True + refresh_data["update_and_reload"] = True return (hook,) @@ -1927,8 +1957,8 @@ class FlipStreamSetMessage: CATEGORY = "FlipStreamViewer" def run(self, message, fontsize, hook=None): - setstate["message"] = btoa_utf8(message) - setstate["message_fontsize"] = f"{fontsize}rem" + refresh_data["message"] = btoa_utf8(message) + refresh_data["message_fontsize"] = f"{fontsize}rem" return (hook,) @@ -1958,7 +1988,7 @@ class FlipStreamSetParam: value = btoa_utf8(value) empty = btoa_utf8(empty) if replace or label not in param or param[label] == empty: - setparam[label] = value + refresh_param[label] = value return (hook,) @@ -2089,6 +2119,7 @@ class FlipStreamGetFrame: "index": ("INT", {"default": 0, "min": 0}), "frames": ("INT", {"default": 1, "min": 1}), "capture_only": ("BOOLEAN", {"default": True}), + "enable": ("BOOLEAN", {"default": True}), } } @@ -2104,15 +2135,20 @@ class FlipStreamGetFrame: else: return frame_mtime - def run(self, index, frames, **kwargs): + def run(self, index, frames, capture_only, enable, **kwargs): + if not enable: + return (None, False) + buf = setframe_buffer if capture_only else frame_buffer images_list = [ - np.array(Image.open(io.BytesIO(frame_buffer[i]))) - for i in range(index, min(index + frames, len(frame_buffer))) + np.array(Image.open(io.BytesIO(buf[i]))) + for i in range(index, min(index + frames, len(buf))) ] if not images_list: - return (torch.zeros((1, 64, 64, 3), dtype=torch.float32), False) - image_batch = np.stack(images_list) - return (torch.from_numpy(image_batch).float() / 255.0, True) + 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: @@ -2763,6 +2799,7 @@ NODE_CLASS_MAPPINGS = { "FlipStreamSelectBox_Samplers": FlipStreamSelectBox_Samplers, "FlipStreamSelectBox_Scheduler": FlipStreamSelectBox_Scheduler, "FlipStreamFileSelect_Checkpoints": FlipStreamFileSelect_Checkpoints, + "FlipStreamFileSelect_Loras": FlipStreamFileSelect_Loras, "FlipStreamFileSelect_VAE": FlipStreamFileSelect_VAE, "FlipStreamFileSelect_ControlNetModel": FlipStreamFileSelect_ControlNetModel, "FlipStreamFileSelect_TensorRT": FlipStreamFileSelect_TensorRT, @@ -2804,6 +2841,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FlipStreamSelectBox_Samplers": "FlipStreamSelectBox_Samplers", "FlipStreamSelectBox_Scheduler": "FlipStreamSelectBox_Scheduler", "FlipStreamFileSelect_Checkpoints": "FlipStreamFileSelect_Checkpoints", + "FlipStreamFileSelect_Loras": "FlipStreamFileSelect_Loras", "FlipStreamFileSelect_VAE": "FlipStreamFileSelect_VAE", "FlipStreamFileSelect_ControlNetModel": "FlipStreamFileSelect_ControlNetModel", "FlipStreamFileSelect_TensorRT": "FlipStreamFileSelect_TensorRT", From 68370b4b820fa169b85d7a219eeff18f6138298b Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Mon, 10 Nov 2025 00:03:26 +0900 Subject: [PATCH 06/19] Update flipstreamviewer.py - Add Secret xor_crypt function. --- flipstreamviewer.py | 59 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index e0aca12..4624530 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -2,6 +2,7 @@ import base64 import hashlib import html import io +import itertools import json import threading import time @@ -145,6 +146,11 @@ div.row { position: relative; } +div#presetXorkeyInputDiv { + display: none; + position: relative; +} + .FlipStreamSlider { width: 50%; } @@ -231,6 +237,10 @@ div.row { display: none; } +#presetXorkey { + display: none; +} + #presetFileSelect, #presetTitleInput, #loraFileSelect, @@ -404,9 +414,10 @@ 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]), + body: JSON.stringify([getStateAsJson(force_state), loraPromptOnly, xorkey]), headers: {"Content-Type": "application/json"} }).then(response => response.json()).then(json => { if (loraPromptOnly) { @@ -449,6 +460,7 @@ function movePresetFile() { function savePreset() { const title = document.getElementById("presetTitleInput").value; + const xorkey = document.getElementById("presetXorkeyInput").value; if (title == "") { alert("Please enter a title to save."); return; @@ -459,7 +471,7 @@ function savePreset() { } fetch("/flipstreamviewer/save_preset", { method: "POST", - body: JSON.stringify([getStateAsJson(), getParamAsJson()]), + body: JSON.stringify([getStateAsJson(), getParamAsJson(), xorkey]), headers: {"Content-Type": "application/json"} }).then(response => { if (response.ok) { @@ -967,6 +979,9 @@ function reloadPage(search="") { 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")); } @@ -1251,7 +1266,7 @@ async def viewer(request):
- Preset + Preset
+
+ +
@@ -1559,14 +1577,31 @@ async def move_lorafile(request): 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 = await request.json() - with open(Path("preset", stt["presetFolder"], stt["presetFile"]), "r") as file: - buf = json.load(file) + 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", "")} @@ -1584,12 +1619,18 @@ async def save_preset(request): if request.remote not in allowed_ips: raise HTTPForbidden() - stt, prm = await request.json() + stt, prm, xorkey = await request.json() state.update(stt) param.update(prm) time.sleep(UPDATE_DELAY) - with open(Path("preset", state["presetFolder"], state["presetTitle"] + ".json"), "w") as file: - json.dump(param, file) + + 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() From 428f2472539ee4da6ccbd16f337392ed2a0338d1 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Wed, 19 Nov 2025 00:42:36 +0900 Subject: [PATCH 07/19] Update flipstreamviewer.py - Fix FlipStreamFileSelect to be selectable collect path. - Fix FlipStreamChat n_ctx, max_tokens option range. - Add pingpong option to FlipStreamViewer. --- flipstreamviewer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index 4624530..8acab54 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -1168,7 +1168,7 @@ async def viewer(request): 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, Path(folder_path, mode))] + 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'] @@ -1810,7 +1810,7 @@ class FlipStreamFileSelect: FOLDER_PATH = "" @staticmethod - def get_filelist(folder_name, folder_path): + def get_filelist(folder_name, folder_path, mode): if folder_name == "checkpoints": return CheckpointLoaderSimple.INPUT_TYPES()["required"]["ckpt_name"][0] elif folder_name == "vae": @@ -1828,14 +1828,14 @@ class FlipStreamFileSelect: 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).glob("*.*")] + 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),), + "default": ([""] + s.get_filelist(s.FOLDER_NAME, s.FOLDER_PATH, ""),), "folder_name": ([s.FOLDER_NAME],), "folder_path": ([s.FOLDER_PATH],), "mode": ("STRING", {"default": ""}), @@ -2556,7 +2556,7 @@ class FlipStreamChat: return { "required": { "model_file": ([path.name for path in Path(folder_paths.models_dir, "LLM").glob("*.gguf")],), - "n_ctx": ("INT", {"default": 2048}), + "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}), @@ -2568,7 +2568,7 @@ class FlipStreamChat: "temperature": ("FLOAT", {"default": 0.2, "min": 0}), "top_p": ("FLOAT", {"default": 0.95, "min": 0}), "seed": ("INT", {"default": -1}), - "max_tokens": ("INT", {"default": 128, "min": 0}), + "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}), @@ -2793,6 +2793,7 @@ class FlipStreamViewer: "fps": ("INT", {"default": 16, "min": 1, "max": 30}), "loramode": ("STRING", {"default": ""}), "reset_updating": ("BOOLEAN", {"default": True}), + "pingpong": ("BOOLEAN", {"default": True}), }, } @@ -2809,10 +2810,10 @@ class FlipStreamViewer: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, tensor, fps, reset_updating, **kwargs): + def run(self, tensor, fps, reset_updating, pingpong, **kwargs): fb = [] buf = (tensor.detach().cpu().numpy() * 255).astype(np.uint8) - if tensor.shape[0] != 1: + 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: From 88ab694ae479f635d5393cefc6a6e0e420ed33e7 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sun, 21 Dec 2025 20:31:09 +0900 Subject: [PATCH 08/19] Update flipstreamviewer.py - Add FlipStreamFileSelect_LLM node. - Add elapsed time bar UI. - Refine FlipStreamSource to support upscale with keep aspect. --- flipstreamviewer.py | 54 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index 8acab54..ded6090 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -67,7 +67,7 @@ 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} +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_updating = None frame_buffer = [] frame_mtime = 0 @@ -318,7 +318,8 @@ function getStateAsJson(force_state={}) { const loraLinkHref = document.getElementById("loraLink").getAttribute("href"); const loraPreviewSrc = document.getElementById("loraPreview").getAttribute("src"); const darker = parseFloat(document.getElementById("darkerRange").value); - var res = { presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, loraRate: loraRate, loraRank: loraRank, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, darker: darker }; + const lastElapsed = document.getElementById("statusLastElapsed").value; + var res = { presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, loraRate: loraRate, loraRank: loraRank, 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; @@ -843,6 +844,10 @@ 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"; @@ -1257,6 +1262,8 @@ async def viewer(request):
+ +
Status @@ -1455,9 +1462,12 @@ async def get_status(request): # Get status info from history remain = server.PromptServer.instance.prompt_queue.get_tasks_remaining() hist = server.PromptServer.instance.prompt_queue.get_history(max_items=1) + status_elapsed = 0 status_info = [] if frame_updating: - status_info.append(f"updating {int(time.time() - frame_updating)}s") + status_elapsed = int(time.time() - frame_updating) + status_info.append(f"updating {status_elapsed}s") + status_info.append(f"q{remain}") info = next(iter(hist.values()))["status"] if hist else None if info: @@ -1469,6 +1479,7 @@ async def get_status(request): 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["log"] = {key: state[key] for key in state if key.endswith("LogBox")} @@ -1811,7 +1822,7 @@ class FlipStreamFileSelect: @staticmethod def get_filelist(folder_name, folder_path, mode): - if folder_name == "checkpoints": + 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] @@ -1877,6 +1888,11 @@ class FlipStreamFileSelect_VAE(FlipStreamFileSelect): 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() @@ -2294,9 +2310,9 @@ class FlipStreamSource: def INPUT_TYPES(s): return { "required": { - "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}), + "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}), }, "optional": { "image": ("IMAGE",), @@ -2312,9 +2328,19 @@ class FlipStreamSource: def run(self, width, height, frames, image=None, vae=None): latent = None if image is not None and not torch.any(image): - image = None - if image is not None and image.shape[3] == 4: - image = image[:,:,:,:3] * image[:,:,:,3:4] + 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: buf = image[:frames] buf = buf.movedim(-1,1) @@ -2325,6 +2351,12 @@ class FlipStreamSource: else: latent = {"samples": torch.zeros([frames, 4, height // 8, width // 8], device=comfy.model_management.intermediate_device())} 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,) @@ -2843,6 +2875,7 @@ NODE_CLASS_MAPPINGS = { "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, @@ -2885,6 +2918,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "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", From 946a0106bb1bb262910926f906bc90284f09fc8a Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sun, 28 Dec 2025 11:52:40 +0900 Subject: [PATCH 09/19] Update flipstreamviewer.py - Add FlipStreamPasteBox. - Change Capture button order. --- flipstreamviewer.py | 125 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index ded6090..e31e8ae 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -170,8 +170,20 @@ div#presetXorkeyInputDiv { display: none; } -.FlipStreamPreviewBox { +.FlipStreamPasteBox { + border: 1px dimgray; max-width: 100%; + min-height: 2em; + max-height: 8em; + object-fit: scale-down; +} + +.FlipStreamPreviewBox { + border: 1px dimgray; + max-width: 100%; + min-height: 2em; + max-height: 8em; + object-fit: scale-down; } .FlipStreamPreviewRoi { @@ -785,6 +797,23 @@ function detailsState() { } 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"); @@ -819,6 +848,7 @@ function hideView() { 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"; @@ -876,6 +906,11 @@ async function refreshView() { 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.paste_mtime[x.id] || 0}`; + } + }); const response = await fetch('/flipstreamviewer/stream/info'); streamInfo = await response.json(); @@ -1044,7 +1079,7 @@ async def preview(request): if request.remote not in allowed_ips: raise HTTPForbidden() - label = request.rel_url.query.get('label', '') + label = request.query.get('label', '') key = label + "PreviewBox" if key in state: data = state[key][1] @@ -1061,6 +1096,38 @@ async def preview_setroi(request): 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', '') + key = label + "PasteBox" + 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/paste_upload") +async def paste_upload(request): + if request.remote not in allowed_ips: raise HTTPForbidden() + label = request.query.get('label', '') + state[label + "PasteBox"] = (time.time(), await request.read()) + 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', '') + key = label + "PasteBox" + if key in state: + del state[key] + return web.Response() + + @server.PromptServer.instance.routes.get("/flipstreamviewer") async def viewer(request): if request.remote not in allowed_ips: @@ -1078,12 +1145,12 @@ async def viewer(request): def add_button(title, capture, update, hook): block[f"{title}"] = f"""
""" - if capture: - block[f"{title}"] += f""" - """ if update: block[f"{title}"] += f""" """ + if capture: + block[f"{title}"] += f""" + """ block[f"{title}"] += f"""
""" @@ -1217,6 +1284,14 @@ async def viewer(request):
""" + 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, log, rows): if not label.isidentifier(): raise RuntimeError(f"{title}: label must contain only valid identifier characters.") @@ -1247,6 +1322,8 @@ async def viewer(request): 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) @@ -1482,6 +1559,7 @@ async def get_status(request): 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["paste_mtime"] = {key: state[key][0] for key in state if key.endswith("PasteBox")} data["log"] = {key: state[key] for key in state if key.endswith("LogBox")} return web.json_response(data) @@ -1951,6 +2029,41 @@ class FlipStreamPreviewBox: 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 state.get(label + "PasteBox", [0])[0] + + def run(self, label, **kwargs): + key = label + "PasteBox" + if key not in state: + return (None, False) + + global frame_updating + frame_updating = time.time() + + img = Image.open(io.BytesIO(state[key][1])) + image = torch.from_numpy(np.array(img)).float()[None, :] / 255.0 + + if image.shape[3] == 4: + image = image[:,:,:,:3] * image[:,:,:,3:4] + + return (image, True) + + class FlipStreamLogBox: @classmethod def INPUT_TYPES(s): @@ -2882,6 +2995,7 @@ NODE_CLASS_MAPPINGS = { "FlipStreamFileSelect_Input": FlipStreamFileSelect_Input, "FlipStreamFileSelect_Output": FlipStreamFileSelect_Output, "FlipStreamPreviewBox": FlipStreamPreviewBox, + "FlipStreamPasteBox": FlipStreamPasteBox, "FlipStreamLogBox": FlipStreamLogBox, "FlipStreamSetUpdateAndReload": FlipStreamSetUpdateAndReload, "FlipStreamSetMessage": FlipStreamSetMessage, @@ -2925,6 +3039,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FlipStreamFileSelect_Input": "FlipStreamFileSelect_Input", "FlipStreamFileSelect_Output": "FlipStreamFileSelect_Output", "FlipStreamPreviewBox": "FlipStreamPreviewBox", + "FlipStreamPasteBox": "FlipStreamPasteBox", "FlipStreamLogBox": "FlipStreamLogBox", "FlipStreamSetUpdateAndReload": "FlipStreamSetUpdateAndReload", "FlipStreamSetMessage": "FlipStreamSetMessage", From fb16962357ddce1cda0fc3ecbf941ede94db9e61 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sun, 28 Dec 2025 11:55:13 +0900 Subject: [PATCH 10/19] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index eb9f15d..ee0d387 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ python -m pip install llama-cpp-python - **FlipStreamFileSelect_Input**: A file selector for ComfyUI input folder. - **FlipStreamFileSelect_Output**: A file selector for ComfyUI output folder. - **FlipStreamPreviewBox**: A box for previewing the input image. You can select the ROI (Region of Interest) in the preview by dragging the mouse. +- **FlipStreamPasteBox**: A box for paste an input image. You can select the box by click once, then you can paste image from clipboard by Ctrl+V or delete image by DEL. - **FlipStreamLogBox**: A box for displaying logs on the screen. ## Other Nodes @@ -157,3 +158,4 @@ python build_sage.py - **visualnobel.json**: Visual nobel like UI using FlipStreamChat. + From 1a7f142c9b9ccfc93dba155b6d2faa7163afe938 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Tue, 30 Dec 2025 23:57:15 +0900 Subject: [PATCH 11/19] Update flipstreamviewer.py - Add FlipStreamTextConcat. - Fix FlipStreamPasteBox. - Refine some code. --- flipstreamviewer.py | 235 +++++++++++++++++++++++++++----------------- 1 file changed, 146 insertions(+), 89 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index e31e8ae..06b4d79 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -79,7 +79,7 @@ class AnyType(str): def __ne__(self, __value: object) -> bool: return False -any = AnyType("*") +anytype = AnyType("*") HEAD=r""" @@ -173,16 +173,16 @@ div#presetXorkeyInputDiv { .FlipStreamPasteBox { border: 1px dimgray; max-width: 100%; - min-height: 2em; - max-height: 8em; + min-height: 32px; + max-height: 160px; object-fit: scale-down; } .FlipStreamPreviewBox { border: 1px dimgray; max-width: 100%; - min-height: 2em; - max-height: 8em; + min-height: 32px; + max-height: 160px; object-fit: scale-down; } @@ -901,6 +901,7 @@ async function refreshView() { } }); 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}`; @@ -997,7 +998,6 @@ async function setFrame() { SCRIPT_QUERY=r""" function onInputDarker(value=-1) { - const container = document.getElementById('mainDialog'); value = parseFloat(value); if (value < 0) { value = document.getElementById("darkerRange").value; @@ -1005,7 +1005,14 @@ function onInputDarker(value=-1) { document.getElementById("darkerRange").value = value; } document.getElementById("darkerValue").innerText = value; - container.style.backgroundColor = 'rgba(0,0,0,' + 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(); @@ -1079,7 +1086,10 @@ async def preview(request): if request.remote not in allowed_ips: raise HTTPForbidden() - label = request.query.get('label', '') + 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] @@ -1090,7 +1100,9 @@ async def preview(request): @server.PromptServer.instance.routes.post("/flipstreamviewer/preview_setroi") async def preview_setroi(request): - if request.remote not in allowed_ips: raise HTTPForbidden() + if request.remote not in allowed_ips: + raise HTTPForbidden() + data = await request.json() param[data['label'] + "PreviewRoi"] = data return web.Response() @@ -1101,7 +1113,10 @@ async def paste(request): if request.remote not in allowed_ips: raise HTTPForbidden() - label = request.query.get('label', '') + label = request.query.get('label') + if not label: + return web.Response(status=400, text="Label required") + key = label + "PasteBox" if key in state: data = state[key][1] @@ -1112,16 +1127,31 @@ async def paste(request): @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', '') - state[label + "PasteBox"] = (time.time(), await request.read()) + 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() + with Image.open(io.BytesIO(data)) as img: + if any(s < 64 for s in img.size): + return web.Response(status=400, text="Image too small") + + state[label + "PasteBox"] = (time.time(), data) 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 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 + "PasteBox" if key in state: del state[key] @@ -1136,13 +1166,13 @@ async def viewer(request): block = {} - def add_section(title, section, hook): + def add_section(title, section, **_): block[f"{title}_{section}"] = f"""
{section}""" - def add_button(title, capture, update, hook): + def add_button(title, capture, update, **_): block[f"{title}"] = f"""
""" if update: @@ -1154,7 +1184,7 @@ async def viewer(request): block[f"{title}"] += f"""
""" - def add_slider(title, label, default, min, max, step): + 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) @@ -1164,14 +1194,14 @@ async def viewer(request): {float(param[label]):g}{label} """ - def add_textbox(title, label, default, rows): + 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): + 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) @@ -1194,7 +1224,7 @@ async def viewer(request): """ - def add_selectbox(title, label, default, listitems): + def add_selectbox(title, label, listitems, **_): listitems = listitems.split(",") if not label.isidentifier(): raise RuntimeError(f"{title}: label must contain only valid identifier characters.") @@ -1203,7 +1233,7 @@ async def viewer(request): param.setdefault(label, "") text_html = f""" """ block[f"{title}_{label}"] = text_html - def add_fileselect(title, label, default, folder_name, folder_path, mode, use_sub, use_move): + 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()): @@ -1232,7 +1262,7 @@ async def viewer(request): text_html += f"""
""" block[f"{title}_{label}"] = text_html - def add_previewbox(title, label, tensor): + 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: @@ -1284,7 +1314,7 @@ async def viewer(request):
""" - def add_pastebox(title, label): + 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""" @@ -1292,7 +1322,7 @@ async def viewer(request): {label} """ - def add_logbox(title, label, log, rows): + 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: @@ -1731,16 +1761,16 @@ class FlipStreamSection: "section": ("STRING", {"default": "Section"}), }, "optional": { - "hook": (any,), + "hook": (anytype,), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, section, hook=None): - return (hook,) + def run(self, **_): + return (True,) class FlipStreamButton: @@ -1752,16 +1782,16 @@ class FlipStreamButton: "update": ("BOOLEAN", {"default": True}), }, "optional": { - "hook": (any,), + "hook": (anytype,), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, hook=None, **kwargs): - return (hook,) + def run(self, **_): + return (True,) class FlipStreamSlider: @@ -1778,15 +1808,16 @@ class FlipStreamSlider: } RETURN_TYPES = ("FLOAT", "INT", "BOOLEAN") + RETURN_NAMES = ("float", "int", "enable") FUNCTION = "run" CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, default, **kwargs): + def IS_CHANGED(cls, label, default, **_): param.setdefault(label, default) - return hash((param[label],)) + return hash(param[label]) - def run(self, label, default, **kwargs): + def run(self, label, default, **_): global frame_updating frame_updating = time.time() param.setdefault(label, default) @@ -1804,20 +1835,22 @@ class FlipStreamTextBox: } } - RETURN_TYPES = ("STRING",) + RETURN_TYPES = ("STRING", "BOOLEAN") + RETURN_NAMES = ("text", "enable") FUNCTION = "run" CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, default, **kwargs): + def IS_CHANGED(cls, label, default, **_): param.setdefault(label, btoa_utf8(default)) - return hash((param[label],)) - - def run(self, label, default, **kwargs): + return hash(param[label]) + + def run(self, label, default, **_): global frame_updating frame_updating = time.time() param.setdefault(label, btoa_utf8(default)) - return (atob_utf8(param[label]),) + text = atob_utf8(param[label]) + return (text, bool(text)) class FlipStreamInputBox: @@ -1832,13 +1865,14 @@ class FlipStreamInputBox: } RETURN_TYPES = ("STRING", "FLOAT", "INT", "BOOLEAN") + RETURN_NAMES = ("text", "float", "int", "enable") FUNCTION = "run" CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, default, **kwargs): + def IS_CHANGED(cls, label, default, **_): param.setdefault(label, default) - return hash((param[label],)) + return hash(param[label]) def floator0(self, v): try: @@ -1846,7 +1880,7 @@ class FlipStreamInputBox: except: return 0 - def run(self, label, default, boxtype, **kwargs): + def run(self, label, default, boxtype, **_): global frame_updating frame_updating = time.time() param.setdefault(label, default) @@ -1868,17 +1902,17 @@ class FlipStreamSelectBox: }, } - RETURN_TYPES = (any, "BOOLEAN",) + RETURN_TYPES = (anytype, "BOOLEAN",) RETURN_NAMES = ("item", "enable",) FUNCTION = "run" CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, **kwargs): + def IS_CHANGED(cls, label, **_): param.setdefault(label, "") - return hash((param[label],)) + return hash(param[label]) - def run(self, label, default, **kwargs): + def run(self, label, default, **_): global frame_updating frame_updating = time.time() param.setdefault(label, "") @@ -1933,17 +1967,17 @@ class FlipStreamFileSelect: } } - RETURN_TYPES = (any, any, "BOOLEAN",) + RETURN_TYPES = (anytype, anytype, "BOOLEAN",) RETURN_NAMES = ("file", "path", "enable",) FUNCTION = "run" CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, **kwargs): + def IS_CHANGED(cls, label, **_): param.setdefault(label, "") - return hash((param[label],)) + return hash(param[label]) - def run(self, label, default, folder_path, **kwargs): + def run(self, label, default, folder_path, **_): global frame_updating frame_updating = time.time() param.setdefault(label, "") @@ -2017,7 +2051,7 @@ class FlipStreamPreviewBox: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, label, tensor, **kwargs): + 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) @@ -2047,7 +2081,7 @@ class FlipStreamPasteBox: def IS_CHANGED(cls, label): return state.get(label + "PasteBox", [0])[0] - def run(self, label, **kwargs): + def run(self, label, **_): key = label + "PasteBox" if key not in state: return (None, False) @@ -2080,7 +2114,7 @@ class FlipStreamLogBox: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, label, log, **kwargs): + def run(self, label, log, **_): state[label + "LogBox"] = btoa_utf8(log) return () @@ -2093,19 +2127,19 @@ class FlipStreamSetUpdateAndReload: "delay_sec": ("FLOAT", {"default": 1, "min": 0.0, "max": 30.0, "step": 0.1}), }, "optional": { - "hook": (any,), + "hook": (anytype,), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, delay_sec, hook=None): + def run(self, delay_sec, **_): time.sleep(delay_sec) refresh_data["update_and_reload"] = True - return (hook,) + return (True,) class FlipStreamSetMessage: @@ -2117,19 +2151,19 @@ class FlipStreamSetMessage: "fontsize": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.1}), }, "optional": { - "hook": (any,), + "hook": (anytype,), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, message, fontsize, hook=None): + def run(self, message, fontsize, **_): refresh_data["message"] = btoa_utf8(message) refresh_data["message_fontsize"] = f"{fontsize}rem" - return (hook,) + return (True,) class FlipStreamSetParam: @@ -2143,23 +2177,23 @@ class FlipStreamSetParam: "b64enc": ("BOOLEAN", {"default": False}), }, "optional": { - "hook": (any,), + "hook": (anytype,), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, label, value, replace, b64enc, hook=None): + def run(self, label, value, replace, b64enc, **_): empty = "" if b64enc: value = btoa_utf8(value) empty = btoa_utf8(empty) if replace or label not in param or param[label] == empty: refresh_param[label] = value - return (hook,) + return (True,) class FlipStreamGetParam: @@ -2184,7 +2218,7 @@ class FlipStreamGetParam: value = param[label] if b64dec: value = atob_utf8(value) - return hash((value,)) + return hash(value) def run(self, label, default, b64dec): global frame_updating @@ -2218,7 +2252,7 @@ class FlipStreamGetPreviewRoi: CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, **kwargs): + def IS_CHANGED(cls, label, **_): roi_data = frozenset(param.get(label + "PreviewRoi", {}).items()) return hash(roi_data) @@ -2264,7 +2298,7 @@ class FlipStreamTextReplace: "replace": ("STRING", {"default": ""}), }, "optional": { - "value": (any,), + "value": (anytype,), } } @@ -2281,6 +2315,27 @@ class FlipStreamTextReplace: return (text,) +class FlipStreamTextConcat: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "joinstr": ("STRING", {"default": "", "multiline": True}), + "text": ("STRING",), + "text2": ("STRING",), + "text3": ("STRING",), + "text4": ("STRING",), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + def run(self, joinstr, text, text2, text3, text4, **_): + return (str(joinstr).join(map(str, filter(None, [text, text2, text3, text4]))),) + + class FlipStreamGetFrame: @classmethod def INPUT_TYPES(s): @@ -2299,13 +2354,13 @@ class FlipStreamGetFrame: CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, capture_only, **kwargs): + def IS_CHANGED(cls, capture_only, **_): if capture_only: return setframe_mtime else: return frame_mtime - def run(self, index, frames, capture_only, enable, **kwargs): + def run(self, index, frames, capture_only, enable, **_): if not enable: return (None, False) buf = setframe_buffer if capture_only else frame_buffer @@ -2480,15 +2535,15 @@ class FlipStreamSwitch: def INPUT_TYPES(s): return { "required": { - "value": (any,), + "value": (anytype,), }, "optional": { - "value_enable": (any,), + "value_enable": (anytype,), "enable": ("BOOLEAN",), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) FUNCTION = "run" CATEGORY = "FlipStreamViewer" @@ -2555,18 +2610,18 @@ class FlipStreamGate: "latent": ("LATENT",), }, "optional": { - "a": (any,), - "b": (any,), - "c": (any,), - "d": (any,), - "e": (any,), - "f": (any,), - "g": (any,), - "h": (any,) + "a": (anytype,), + "b": (anytype,), + "c": (anytype,), + "d": (anytype,), + "e": (anytype,), + "f": (anytype,), + "g": (anytype,), + "h": (anytype,) } } - RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", any, any, any, any, any, any, any, any) + 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" @@ -2943,7 +2998,7 @@ class FlipStreamViewer: } @classmethod - def IS_CHANGED(cls, allowip, idle, loramode, **kwargs): + 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 @@ -2955,7 +3010,7 @@ class FlipStreamViewer: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, tensor, fps, reset_updating, pingpong, **kwargs): + def run(self, tensor, fps, reset_updating, pingpong, **_): fb = [] buf = (tensor.detach().cpu().numpy() * 255).astype(np.uint8) if tensor.shape[0] != 1 and pingpong: @@ -3005,6 +3060,7 @@ NODE_CLASS_MAPPINGS = { "FlipStreamGetPreviewRoi": FlipStreamGetPreviewRoi, "FlipStreamImageSize": FlipStreamImageSize, "FlipStreamTextReplace": FlipStreamTextReplace, + "FlipStreamTextConcat": FlipStreamTextConcat, "FlipStreamScreenGrabber": FlipStreamScreenGrabber, "FlipStreamVideoInput": FlipStreamVideoInput, "FlipStreamSource": FlipStreamSource, @@ -3049,6 +3105,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FlipStreamGetPreviewRoi": "FlipStreamGetPreviewRoi", "FlipStreamImageSize": "FlipStreamImageSize", "FlipStreamTextReplace": "FlipStreamTextReplace", + "FlipStreamTextConcat": "FlipStreamTextConcat", "FlipStreamScreenGrabber": "FlipStreamScreenGrabber", "FlipStreamVideoInput": "FlipStreamVideoInput", "FlipStreamSource": "FlipStreamSource", From 7520526dc15d72a5fe9b625cf1ea7e63cc204242 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Tue, 30 Dec 2025 23:58:38 +0900 Subject: [PATCH 12/19] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ee0d387..3d8afc0 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ python -m pip install llama-cpp-python - **FlipStreamGetPreviewRoi**: A node for obtaining the preview ROI (Region of Interest) selection. - **FlipStreamImageSize**: A node for getting image size. - **FlipStreamTextReplace**: A node for replacing text. It will output the result of `text.replace(find, replace.format(value))`. +- **FlipStreamTextConcat**: A node for concatenate text. - **FlipStreamScreenGrabber**: A node for grab multiframe screenshots. - **FlipStreamSource**: A node for prepare image or latent source. - **FlipStreamSwitchImage**: A node for switching images. @@ -159,3 +160,4 @@ python build_sage.py + From ad959afa21d23bd85eec8c643fc864fabca102b9 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 08:17:57 +0900 Subject: [PATCH 13/19] Update flipstreamviewer.py - Add loraMode selection and @mode{} format. - Add FlipStreamLoraMode to set loraMode. - Add FlipStreamLoadLora like Lora Tag Loader, it can select mode to apply @mode{} section. - Refine FlipStreamPasteBox. - Refine updating status control to check Comfy task remaining. - Add FlipStreamCurrent to show any text in status info. - Change Update button to Run button. - Change FlipStreamSetUpdateAndReload to FlipStreamRunOnce. - Change progress bar range to 120 sec. - Add FlipStreamSetState, FlipStreamGetState. - Change FlipStreamGetParam outputs - Add frame trimming options for FlipStreamGetFrame. - Add FlipStreamSizeSelect. - Add FlipStreamViewerSimple, FlipStreamAllowIp. - Add FlipStreamSaveApiWorkflow, FlipStreamRunApiWorkflow. - Add FlipStreamFree. - Add FlipStreamShutdown. - Add experimental nodes FlipStreamAnd, FlipStreamOr, FlipStreamGet, FlipStreamChatJson. - Depricated FlipStreamGate, FlipStreamSegMask. - And some minor changes. --- flipstreamviewer.py | 1120 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 888 insertions(+), 232 deletions(-) diff --git a/flipstreamviewer.py b/flipstreamviewer.py index 06b4d79..05d5723 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -6,6 +6,8 @@ import itertools import json import threading import time +import subprocess +import re from pathlib import Path import imageio.v3 as iio @@ -56,19 +58,21 @@ def btoa_utf8(value): return base64.b64encode(value.encode("utf-8")).decode() def atob_utf8(value): - if not 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_updating = None frame_buffer = [] frame_mtime = 0 frame_fps = 16 @@ -161,6 +165,7 @@ div#presetXorkeyInputDiv { .FlipStreamSelectBox, .FlipStreamFolderSelect, +.FlipStreamSizeSelect, .FlipStreamFileSelect, .FlipStreamMoveFileSelect { width: 100%; @@ -239,6 +244,7 @@ div#presetXorkeyInputDiv { #presetFolderSelect, #movePresetSelect, +#loraModeSelect, #loraFolderSelect, #moveLoraSelect { width: 100%; @@ -286,7 +292,7 @@ div#presetXorkeyInputDiv { #loraLink img { max-width: 100%; - max-height: 8em; + max-height: 160px; width: auto; height: auto; margin: auto; @@ -320,6 +326,7 @@ 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"); @@ -331,7 +338,7 @@ function getStateAsJson(force_state={}) { 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, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, darker: darker, lastElapsed: lastElapsed }; + 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; @@ -348,6 +355,7 @@ function getParamAsJson(force_param={}) { 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; @@ -364,13 +372,16 @@ function setParam(param) { } } -function updateParam(reload=false, force_state={}, force_param={}, search="") { +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); } @@ -438,6 +449,7 @@ function loadPreset(loraPromptOnly=false, force_state={}, search="") { document.getElementById("presetTitleInput").value = json.presetTitle; } else { + fetch('/flipstreamviewer/run_once'); reloadPage(search); } }).catch(error => { @@ -539,81 +551,64 @@ function moveFile(folder_path, mode, label) { SCRIPT_LORA=r""" function selectLoraFile() { - const loraFolder = document.getElementById("loraFolderSelect").value; const loraFileSelect = document.getElementById("loraFileSelect"); - const loraName = loraFileSelect.options[loraFileSelect.selectedIndex].text; const loraFile = loraFileSelect.value; - const loraRate = document.getElementById("loraRate").value; - const loraTagSelect = document.getElementById("loraTagSelect"); - const loraInput = document.getElementById("loraInput"); - const loraLink = document.getElementById("loraLink"); - const loraPreview = document.getElementById("loraPreview"); - - if (loraFile) { - const re = new RegExp("[\\n]?", "g"); - if (loraInput.value.search(re) == -1) { - if (loraInput.value.trim() != "") { - loraInput.value += "\n"; - } - loraInput.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 => { - loraTagSelect.innerHTML = ""; - - item = document.createElement("option"); - item.value = ""; - item.text = "tags"; - loraTagSelect.appendChild(item); - + const tagSelect = document.getElementById("loraTagSelect"); + tagSelect.innerHTML = ''; if (json.ss_tag_frequency) { - const datasets = JSON.parse(json.ss_tag_frequency); - const tags = {}; - for (const setName in datasets) { - const set = datasets[setName]; - for (const t in set) { - if (t in tags) { - tags[t] += set[t]; - } else { - tags[t] = set[t]; - } - } - } - const sorted_tags = Object.entries(tags).sort((a, b) => b[1] - a[1]); - for (const i in sorted_tags) { - item = document.createElement("option"); - item.value = sorted_tags[i][0].trim(); - item.text = sorted_tags[i][0].trim() + ":" + sorted_tags[i][1]; - loraTagSelect.appendChild(item); + 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)); + }); } - - loraLink.href = json._lorapreview_href || ""; - loraPreview.src = json._lorapreview_src || ""; + 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 loraFile = loraFileSelect.value; const loraRate = document.getElementById("loraRate").value; const loraInput = document.getElementById("loraInput"); - if (loraFile) { - const re = new RegExp("[\\n]?", "g"); - if (loraInput.value.search(re) >= 0) { - loraInput.value = loraInput.value.replace(re, ""); + + 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 { - loraInput.value += "\n"; + 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 = ""; @@ -643,25 +638,18 @@ function moveLoraFile() { SCRIPT_TAG=r""" function toggleTag() { - const loraTagSelect = document.getElementById("loraTagSelect"); - const loraTag = document.getElementById("loraTagSelect").value; - const loraRank = parseInt(document.getElementById("loraRank").value); - const loraInput = document.getElementById("loraInput"); - if (loraTag) { - const re = new RegExp("^\\s*" + loraTag + "\\s*(,\\s*|\n|$)|,\\s*" + loraTag + "\\s*(,|\n|$)", "g"); - if (loraInput.value.search(re) >= 0) { - loraInput.value = loraInput.value.replace(re, "$2") - } else { - if (loraInput.value) { - loraInput.value += ", " + loraTag; - } else { - loraInput.value = loraTag; - } - } + 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 { - for (var i = 1; i < Math.min(loraRank + 1, loraTagSelect.length); i++) { - loraInput.value += ", " + loraTagSelect.options[i].value; - } + 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'); } } @@ -895,9 +883,9 @@ async function refreshView() { } document.querySelectorAll('.FlipStreamLogBox').forEach(async x => { if (x.id in data.log) { - x.innerText = atob_utf8(data.log[x.id]) || ""; + x.value = atob_utf8(data.log[x.id]) || ""; } else { - x.innerText = ""; + x.value = ""; } }); if (streamViewFlag) { @@ -909,7 +897,7 @@ async function refreshView() { }); document.querySelectorAll('.FlipStreamPasteBox').forEach(async x => { if (x.src != "") { - x.src = `/flipstreamviewer/paste?label=${x.name}&mtime=${data.paste_mtime[x.id] || 0}`; + x.src = `/flipstreamviewer/paste?label=${x.name}&mtime=${data.mtime[x.name+'_mtime'] || 0}`; } }); @@ -932,9 +920,6 @@ async function refreshView() { updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps); } } - if (data.update_and_reload) { - updateParam(true); - } } setInterval(refreshView, 1000); """ @@ -1041,7 +1026,7 @@ function parseQueryParam() { if (p.has("presetFolder")) { const presetFolder = document.getElementById("presetFolderSelect").value; if (presetFolder != p.get("presetFolder")) { - updateParam(true, { presetFolder: p.get("presetFolder") }, {}, p.toString()); + updateParam(true, false, { presetFolder: p.get("presetFolder") }, {}, p.toString()); return; } } @@ -1057,6 +1042,14 @@ 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: @@ -1117,9 +1110,8 @@ async def paste(request): if not label: return web.Response(status=400, text="Label required") - key = label + "PasteBox" - if key in state: - data = state[key][1] + if label + "_thumbnail" in state: + data = state[label + "_thumbnail"] else: data = b"" return web.Response(body=data, headers={"Content-Type": "image/png"}) @@ -1135,11 +1127,13 @@ async def paste_upload(request): return web.Response(status=400, text="Label required") data = await request.read() - with Image.open(io.BytesIO(data)) as img: - if any(s < 64 for s in img.size): - return web.Response(status=400, text="Image too small") - - state[label + "PasteBox"] = (time.time(), data) + 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() @@ -1152,9 +1146,10 @@ async def paste_upload(request): if not label: return web.Response(status=400, text="Label required") - key = label + "PasteBox" - if key in state: - del state[key] + if label in state: + del state[label] + del state[label + "_thumbnail"] + del state[label + "_mtime"] return web.Response() @@ -1172,12 +1167,12 @@ async def viewer(request):
{section}""" - def add_button(title, capture, update, **_): + def add_button(title, run, capture, **_): block[f"{title}"] = f"""
""" - if update: + if run: block[f"{title}"] += f""" - """ + """ if capture: block[f"{title}"] += f""" """ @@ -1209,19 +1204,19 @@ async def viewer(request): 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, **_): @@ -1241,6 +1236,23 @@ async def viewer(request): """ 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.") @@ -1329,10 +1341,10 @@ async def viewer(request): block[f"{title}_{label}"] = f""" """ - hist = server.PromptServer.instance.prompt_queue.get_history(max_items=1) - nodedict = next(iter(hist.values()))["prompt"][2] if hist else None - if nodedict: - for node in nodedict.values(): + 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"] @@ -1348,6 +1360,8 @@ async def viewer(request): 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": @@ -1360,6 +1374,9 @@ async def viewer(request): text_html = f"""{HEAD}
+
+ +
Input {"".join([x[1] for x in sorted(block.items())])} @@ -1369,8 +1386,8 @@ async def viewer(request):
- - + +
Status @@ -1411,6 +1428,10 @@ async def viewer(request):
Lora +
- + - +
@@ -1566,20 +1587,30 @@ async def get_status(request): if request.remote not in allowed_ips: raise HTTPForbidden() - # Get status info from history - remain = server.PromptServer.instance.prompt_queue.get_tasks_remaining() - hist = server.PromptServer.instance.prompt_queue.get_history(max_items=1) status_elapsed = 0 status_info = [] - if frame_updating: - status_elapsed = int(time.time() - frame_updating) - status_info.append(f"updating {status_elapsed}s") + + 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 = next(iter(hist.values()))["status"] if hist else None - if info: - errinfo = info["messages"][2][1] - status_info.append(info["status_str"]) + + 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() @@ -1589,7 +1620,7 @@ async def get_status(request): 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["paste_mtime"] = {key: state[key][0] for key in state if key.endswith("PasteBox")} + 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) @@ -1769,8 +1800,8 @@ class FlipStreamSection: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, **_): - return (True,) + def run(self, hook=True, **_): + return (hook,) class FlipStreamButton: @@ -1778,8 +1809,8 @@ class FlipStreamButton: def INPUT_TYPES(s): return { "required": { + "run": ("BOOLEAN", {"default": True}), "capture": ("BOOLEAN", {"default": True}), - "update": ("BOOLEAN", {"default": True}), }, "optional": { "hook": (anytype,), @@ -1790,8 +1821,8 @@ class FlipStreamButton: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, **_): - return (True,) + def run(self, hook=True, **_): + return (hook,) class FlipStreamSlider: @@ -1818,8 +1849,6 @@ class FlipStreamSlider: return hash(param[label]) def run(self, label, default, **_): - global frame_updating - frame_updating = time.time() param.setdefault(label, default) return (float(param[label]), int(float(param[label])), bool(float(param[label]))) @@ -1846,8 +1875,6 @@ class FlipStreamTextBox: return hash(param[label]) def run(self, label, default, **_): - global frame_updating - frame_updating = time.time() param.setdefault(label, btoa_utf8(default)) text = atob_utf8(param[label]) return (text, bool(text)) @@ -1871,22 +1898,18 @@ class FlipStreamInputBox: @classmethod def IS_CHANGED(cls, label, default, **_): - param.setdefault(label, default) - return hash(param[label]) - - def floator0(self, v): - try: - return float(v) - except: - return 0 + return hash(param.get(label)) def run(self, label, default, boxtype, **_): - global frame_updating - frame_updating = time.time() param.setdefault(label, default) - t = param[label] - v = self.floator0(t) - return (t, v, int(v), bool(t if boxtype == "text" else v)) + text = param[label] + enable = bool(text) + try: + num = float(text) + enable = bool(num) + except: + num = 0 + return (text, num, int(num), enable) class FlipStreamSelectBox: @@ -1913,8 +1936,6 @@ class FlipStreamSelectBox: return hash(param[label]) def run(self, label, default, **_): - global frame_updating - frame_updating = time.time() param.setdefault(label, "") item = param[label] if param[label] else default return (item, param[label] != "") @@ -1928,6 +1949,68 @@ 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 = "" @@ -1978,8 +2061,6 @@ class FlipStreamFileSelect: return hash(param[label]) def run(self, label, default, folder_path, **_): - global frame_updating - frame_updating = time.time() param.setdefault(label, "") file = param[label] if param[label] else default return (file, str(Path(folder_path, file)), param[label] != "") @@ -2079,23 +2160,12 @@ class FlipStreamPasteBox: @classmethod def IS_CHANGED(cls, label): - return state.get(label + "PasteBox", [0])[0] + return hash(state.get(label + "_mtime")) def run(self, label, **_): - key = label + "PasteBox" - if key not in state: + if label not in state: return (None, False) - - global frame_updating - frame_updating = time.time() - - img = Image.open(io.BytesIO(state[key][1])) - image = torch.from_numpy(np.array(img)).float()[None, :] / 255.0 - - if image.shape[3] == 4: - image = image[:,:,:,:3] * image[:,:,:,3:4] - - return (image, True) + return (state[label], True) class FlipStreamLogBox: @@ -2119,12 +2189,11 @@ class FlipStreamLogBox: return () -class FlipStreamSetUpdateAndReload: +class FlipStreamRunOnce: @classmethod def INPUT_TYPES(s): return { "required": { - "delay_sec": ("FLOAT", {"default": 1, "min": 0.0, "max": 30.0, "step": 0.1}), }, "optional": { "hook": (anytype,), @@ -2136,10 +2205,11 @@ class FlipStreamSetUpdateAndReload: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, delay_sec, **_): - time.sleep(delay_sec) - refresh_data["update_and_reload"] = True - return (True,) + def run(self, hook=True, **_): + server.PromptServer.instance.send_sync("FlipStreamViewer_run_once", {}) + global refresh_updating + refresh_updating = 0 + return (hook,) class FlipStreamSetMessage: @@ -2160,10 +2230,128 @@ class FlipStreamSetMessage: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, message, fontsize, **_): + def run(self, message, fontsize, hook=True, **_): refresh_data["message"] = btoa_utf8(message) refresh_data["message_fontsize"] = f"{fontsize}rem" - return (True,) + 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: @@ -2186,17 +2374,20 @@ class FlipStreamSetParam: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, label, value, replace, b64enc, **_): + 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 (True,) + return (hook,) class FlipStreamGetParam: + CACHED_LASTGET = {} + @classmethod def INPUT_TYPES(s): return { @@ -2205,30 +2396,114 @@ class FlipStreamGetParam: "default": ("STRING", {"default": ""}), "b64dec": ("BOOLEAN", {"default": False}), }, + "optional": { + "hook": (anytype,), + }, + "hidden": { + "unique_id": "UNIQUE_ID" + } } - RETURN_TYPES = ("STRING",) + RETURN_TYPES = (anytype, "FLOAT", "INT", "BOOLEAN", "BOOLEAN") + RETURN_NAMES = ("text", "float", "int", "enable", "changed") FUNCTION = "run" CATEGORY = "FlipStreamViewer" @classmethod - def IS_CHANGED(cls, label, default, b64dec): - value = default - if label in param: - value = param[label] - if b64dec: - value = atob_utf8(value) - return hash(value) + def IS_CHANGED(cls, label, **_): + return hash(param.get(label)) - def run(self, label, default, b64dec): - global frame_updating - frame_updating = time.time() - value = default - if label in param: - value = param[label] - if b64dec: - value = atob_utf8(value) - return (value,) + 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: @@ -2257,8 +2532,6 @@ class FlipStreamGetPreviewRoi: return hash(roi_data) def run(self, label, default_left, default_top, default_right, default_bottom, width, height): - global frame_updating - frame_updating = time.time() 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 @@ -2321,10 +2594,10 @@ class FlipStreamTextConcat: return { "required": { "joinstr": ("STRING", {"default": "", "multiline": True}), - "text": ("STRING",), - "text2": ("STRING",), - "text3": ("STRING",), - "text4": ("STRING",), + "enable": ("BOOLEAN", {"default": True}) + }, + "optional": { + **{f"text{i}": ("STRING", {"default": ""}) for i in range(20)} } } @@ -2332,8 +2605,17 @@ class FlipStreamTextConcat: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, joinstr, text, text2, text3, text4, **_): - return (str(joinstr).join(map(str, filter(None, [text, text2, text3, text4]))),) + 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: @@ -2481,6 +2763,8 @@ class FlipStreamSource: "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",), @@ -2488,12 +2772,12 @@ class FlipStreamSource: } } - RETURN_TYPES = ("IMAGE", "LATENT",) - RETURN_NAMES = ("image", "latent",) + RETURN_TYPES = ("IMAGE", "LATENT", "BOOLEAN") + RETURN_NAMES = ("image", "latent", "enable") FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, width, height, frames, image=None, vae=None): + 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 @@ -2509,15 +2793,16 @@ class FlipStreamSource: height = image.shape[1] if not frames: frames = image.shape[0] - if image is not None and image.shape[0] >= frames: - buf = image[:frames] + 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", "centor") + 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 @@ -2527,7 +2812,7 @@ class FlipStreamSource: 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,) + return (image, latent, False) class FlipStreamSwitch: @@ -2700,7 +2985,7 @@ class FlipStreamSegMask: # Load model if FlipStreamSegMask.model is None: - FlipStreamSegMask.model = transformers.AutoModelForCausalLM.from_pretrained( + FlipStreamSegMask.model = transformers.Florence2ForConditionalGeneration.from_pretrained( model_id, trust_remote_code=True, attn_implementation='sdpa', @@ -2751,6 +3036,8 @@ class FlipStreamSegMask: class FlipStreamChat: + CACHED_OUTPUT = {} + @classmethod def INPUT_TYPES(cls): return { @@ -2772,25 +3059,29 @@ class FlipStreamChat: "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}) + "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") + RETURN_NAMES = ("chat_model", "response", "messages") FUNCTION = "run" CATEGORY = "FlipStreamViewer" def __init__(self): self.model = None - self.messages = [] self.system = None + self.messages = [] - def load_model(self, model_file, n_ctx, n_gpu_layers): + 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) @@ -2798,13 +3089,23 @@ class FlipStreamChat: 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): - self.model.close() - self.model._FlipStreamChat_is_closed = True + 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: @@ -2822,37 +3123,34 @@ class FlipStreamChat: 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, chat_model=None, messages=None, **kwargs): - if unload_other_models: - comfy.model_management.unload_all_models() - comfy.model_management.soft_empty_cache(True) - try: - comfy.gc.collect() - torch.cuda.empty_cache() - torch.cuda.ipc_collect() - except: - pass + 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) + 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) - output = res["content"] + cache[unique_id] = res["content"] else: res = self.chat(system, user, stop, messages, **kwargs) - output = res["content"] + cache[unique_id] = res["content"] if res["role"] == "assistant": - messages.append(dict(role="assistant", content=output)) + messages.append(dict(role="assistant", content=cache[unique_id])) self.messages = messages if close_after_use: self.close_model() - return (self.model, output, messages) + return (self.model, cache.get(unique_id), messages) class FlipStreamParseJson: @@ -2864,14 +3162,18 @@ class FlipStreamParseJson: "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): + def run(self, json_input, keys, joinstr, ignore_error, enable): + if not enable: + return ("",) + value = [] try: for key in keys.split("\n"): @@ -2882,6 +3184,72 @@ class FlipStreamParseJson: 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): @@ -2992,7 +3360,6 @@ class FlipStreamViewer: "idle": ("FLOAT", {"default": 1.0, "min": 0.0}), "fps": ("INT", {"default": 16, "min": 1, "max": 30}), "loramode": ("STRING", {"default": ""}), - "reset_updating": ("BOOLEAN", {"default": True}), "pingpong": ("BOOLEAN", {"default": True}), }, } @@ -3010,7 +3377,11 @@ class FlipStreamViewer: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, tensor, fps, reset_updating, pingpong, **_): + 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: @@ -3021,17 +3392,268 @@ class FlipStreamViewer: img.save(output, format="PNG", compress_level=STREAM_COMPRESSION) fb.append(output.getvalue()) global frame_buffer - global frame_updating global frame_mtime global frame_fps frame_buffer = fb - if reset_updating: - frame_updating = None 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, @@ -3040,6 +3662,8 @@ NODE_CLASS_MAPPINGS = { "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, @@ -3052,10 +3676,15 @@ NODE_CLASS_MAPPINGS = { "FlipStreamPreviewBox": FlipStreamPreviewBox, "FlipStreamPasteBox": FlipStreamPasteBox, "FlipStreamLogBox": FlipStreamLogBox, - "FlipStreamSetUpdateAndReload": FlipStreamSetUpdateAndReload, + "FlipStreamRunOnce": FlipStreamRunOnce, "FlipStreamSetMessage": FlipStreamSetMessage, + "FlipStreamAnd": FlipStreamAnd, + "FlipStreamOr": FlipStreamOr, + "FlipStreamSetState": FlipStreamSetState, + "FlipStreamGetState": FlipStreamGetState, "FlipStreamSetParam": FlipStreamSetParam, "FlipStreamGetParam": FlipStreamGetParam, + "FlipStreamGet": FlipStreamGet, "FlipStreamGetFrame": FlipStreamGetFrame, "FlipStreamGetPreviewRoi": FlipStreamGetPreviewRoi, "FlipStreamImageSize": FlipStreamImageSize, @@ -3072,9 +3701,19 @@ NODE_CLASS_MAPPINGS = { "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 = { @@ -3085,6 +3724,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "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", @@ -3097,10 +3738,15 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FlipStreamPreviewBox": "FlipStreamPreviewBox", "FlipStreamPasteBox": "FlipStreamPasteBox", "FlipStreamLogBox": "FlipStreamLogBox", - "FlipStreamSetUpdateAndReload": "FlipStreamSetUpdateAndReload", + "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", @@ -3112,12 +3758,22 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FlipStreamSwitch": "FlipStreamSwitch", "FlipStreamSwitchImage": "FlipStreamSwitchImage", "FlipStreamSwitchLatent": "FlipStreamSwitchLatent", - "FlipStreamGate": "FlipStreamGate", + "FlipStreamGate": "FlipStreamGate(deprecated)", "FlipStreamRembg": "FlipStreamRembg", - "FlipStreamSegMask": "FlipStreamSegMask", + "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", } From cd1ac4bd9e6cd4c0c09edcbeb3879443335f9c74 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 08:19:01 +0900 Subject: [PATCH 14/19] Update __init__.py --- __init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index 5f80308..07ef384 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,2 @@ -from .flipstreamviewer import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] +from .flipstreamviewer import WEB_DIRECTORY, NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS +__all__ = ['WEB_DIRECTORY', 'NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] From 47d0e5110cfadfc57b4f89fa446fbecfba662b05 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 08:22:37 +0900 Subject: [PATCH 15/19] Create flipstreamviewer.js --- web/flipstreamviewer.js | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 web/flipstreamviewer.js diff --git a/web/flipstreamviewer.js b/web/flipstreamviewer.js new file mode 100644 index 0000000..0e2e912 --- /dev/null +++ b/web/flipstreamviewer.js @@ -0,0 +1,67 @@ +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; + +app.registerExtension({ + name: "FlipStreamViewer.RunOnce", + async setup() { + api.addEventListener("FlipStreamViewer_run_once", async () => { + await api.interrupt(); + await api.fetchApi("/queue", { + method: "POST", + body: JSON.stringify({ clear: true }), + }); + await app.queuePrompt(0, 1); + }); + } +}); + +app.registerExtension({ + name: "FlipStreamViewer.FlipStreamChatJson", + async beforeRegisterNodeDef(t, d) { + const config = { + "FlipStreamGet": { in: 0, out: 2, range: 20, iname:'label', oname:'value', useo: true }, + "FlipStreamChatJson": { in: 9, out: 3, range: 20, iname:'label', oname:'value', useo: true }, + "FlipStreamTextConcat": { in: 2, out: 1, range: 20, iname:'text', oname:'', useo: false } + }; + const CFG = config[d.name]; + if (!CFG) return; + + const sync = (n) => { + const w = n.widgets; + if (CFG.useo) { + while (n.outputs.length > w.length + CFG.out - CFG.in) { + if (n.outputs.length < CFG.out || n.outputs[n.outputs.length - 1]?.links?.length) break; + n.removeOutput(n.outputs.length - 1); + n.outputs_dirty = true; + } + } + if (w.length < CFG.in + CFG.range && (w.at(-1)?.value || n.inputs[w.length - 1]?.link !== null)) { + if (CFG.useo) { + n.addOutput(`${CFG.oname}${w.length - CFG.in}`, "*"); + n.outputs_dirty = true; + } + const inputName = `${CFG.iname}${w.length - CFG.in}`; + n.addWidget("string", inputName, "", () => {}, { forceInput: true }); + n.inputs_dirty = true; + } + while (w.length > CFG.in + 1 && (!w.at(-1).value && n.inputs[w.length - 1]?.link === null) && (!w.at(-2).value && n.inputs[w.length - 2]?.link === null)) { + if (n.outputs[w.length + CFG.out - CFG.in - 1]?.links?.length) break; + if (CFG.useo) { + n.removeOutput(w.length + CFG.out - CFG.in - 1); + n.outputs_dirty = true; + } + w.pop(); + n.inputs_dirty = true; + } + n.setSize(n.computeSize()); + }; + + t.prototype.onNodeCreated = function() { sync(this); }; + t.prototype.onWidgetChanged = function() { sync(this); }; + t.prototype.onConnectionsChange = function(type, slot, connected, info) { sync(this);}; + + t.prototype.onConfigure = function(c) { + c.widgets_values.forEach((v, i) => { if (this.widgets[i]) this.widgets[i].value = v; sync(this);}); + }; + } +}); From beea6b09cd5a0e6445d43d09c101a83da6a09fc0 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 08:55:51 +0900 Subject: [PATCH 16/19] Update README.md --- README.md | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3d8afc0..71f2881 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ The following custom nodes can be used as needed: - ComfyUI-Inspyrenet-Rembg: For FlipStreamRembg -- ComfyUI-Florence2: For FlipStreamSegMask - ComfyUI-Frame-Interpolation: For FlipStreamFilmVfi The following custom nodes may also be used within workflows: @@ -59,7 +58,7 @@ python -m pip install llama-cpp-python - **UI Node Ordering**: These UI nodes are ordered by their titles, and you can change the node titles to something like '10.steps' or '11.cfg'. You can also click on the title to compact the node on the workflow. - **Label Input**: Each UI node has a 'label' input, which should be a unique identifier used as the parameter name. 'lora' is internally used, so they cannot be used for labels. - **Enable Output**: Each UI node also has an 'enable' output to use with 'Control Bridge' for switching to bypass some nodes or FlipStreamSwitch* for switching input. -- **Right Panel**: You can operate several prepared functions such as Status, Darker, Tagger, Preset, and Lora. +- **Right Panel**: You can operate several prepared functions such as Status, Darker, Preset, and Lora. - **Status**: The right panel shows ComfyUI status and error information. - **Darker**: You can adjust the image brightness on the viewer using the darker parameter. It can also be set via query: `http://localhost:8188/flipstreamviewer?darker=0.33` @@ -74,14 +73,16 @@ python -m pip install llama-cpp-python ## UI Nodes - **FlipStreamSection**: A section label for UI Nodes. -- **FlipStreamButton**: Capture and Update buttons. The Capture button can be use with FlipStreamGetFrame to get captured image. +- **FlipStreamButton**: Run and Capture buttons. The Capture button can be use with FlipStreamGetFrame to get captured image. - **FlipStreamSlider**: A slider for adjusting values. - **FlipStreamTextBox**: A text box for inputting multiline text. - **FlipStreamInputBox**: An input box for various boxtype inputs. The 'U' button means update. You can choose special boxtype 'seed' or 'r4d' with the 'R' button, which means randomize. The boxtype 'r4d' can be used to generate a 4-digit part of a prompt like 'MOV_{num}'. In this case, FlipStreamTextReplace can help find '{num}' and replace with the output of the input box. - **FlipStreamSelectBox_Samplers**: A select box for choosing samplers. - **FlipStreamSelectBox_Scheduler**: A select box for choosing schedulers. +- **FlipStreamSizeSelect**: A select box for choosing size by aspect ratio. The size can also get using FlipStreamGetSize. - **FlipStreamFileSelect_Checkpoints**: A file selector for checkpoints. 'mode' is used to select a subfolder such as 'sd15', 'sdxl', 'pony', or 'flux' in 'checkpoints'. 'use_sub' means using subfolders in the 'mode' folder. 'use_move' means using the move file selector. - **FlipStreamFileSelect_VAE**: A file selector for VAE models. +- **FlipStreamFileSelect_LLM**: A file selector for LLM models. - **FlipStreamFileSelect_ControlNetModel**: A file selector for ControlNet models. - **FlipStreamFileSelect_TensorRT**: A file selector for TensorRT models. It may be used with ComfyUI_TensorRT. - **FlipStreamFileSelect_AnimateDiffModel**: A file selector for AnimateDiff models. It may be used with ComfyUI-AnimateDiff-Evolved. @@ -93,24 +94,31 @@ python -m pip install llama-cpp-python ## Other Nodes -- **FlipStreamSetUpdateAndReload**: Updates parameters and reloads pages after a delay. +- **FlipStreamRunOnce**: Updates parameters, reloads pages, and run the current workflow once. - **FlipStreamSetMessage**: Sets a message for the message box at the bottom of the center panel. +- **FlipStreamAnd(experimental)**: Multiple inputs AND operation node. +- **FlipStreamOr(experimental)**: Multiple inputs OR operation node. +- **FlipStreamSetState**: A node for setting internal state. +- **FlipStreamGetState**: A node for getting internal state. - **FlipStreamSetParam**: A node for setting parameters. However, it needs a reload to update the value in the viewer UI. - **FlipStreamGetParam**: A node for getting parameters. 'lora' is a prepared parameter that contains text at the Lora input. Some prompts can contain '{lora}' and FlipStreamTextReplace can help find '{lora}' and replace it with the output of the get param node of 'lora'. 'b64dec' is true for 'lora' and other 'FlipStreamTextBox' parameters, because these multiline parameters are internally base64 encoded. +- **FlipStreamGet(experimental)**: A node for getting internal state and parameters with independent node cache. - **FlipStreamGetFrame**: A node for get image from frame buffer. It can be use with FlipStreamButton - Capture button to get captured image. - **FlipStreamGetPreviewRoi**: A node for obtaining the preview ROI (Region of Interest) selection. - **FlipStreamImageSize**: A node for getting image size. - **FlipStreamTextReplace**: A node for replacing text. It will output the result of `text.replace(find, replace.format(value))`. - **FlipStreamTextConcat**: A node for concatenate text. - **FlipStreamScreenGrabber**: A node for grab multiframe screenshots. +- **FlipStreamVideoInput**: A node for input video frames. - **FlipStreamSource**: A node for prepare image or latent source. - **FlipStreamSwitchImage**: A node for switching images. - **FlipStreamSwitchLatent**: A node for switching latents. -- **FlipStreamGate**: A node for consolidating input timing to ensure that inputs to the sampler do not occur sequentially. +- **FlipStreamGate(deprecated)**: A node for consolidating input timing to ensure that inputs to the sampler do not occur sequentially. - **FlipStreamRembg**: A node for remove background. It depends on ComfyUI-Inspyrenet-Rembg. -- **FlipStreamSegMask**: A node for segmentation masks. The target can contain multiple words separated by commas for segmentation. It will use the 'microsoft/Florence-2-large' model, which you can download using the DownloadAndLoadFlorence2Model node of ComfyUI-Florence2. Segmentation sometimes fails, so you may need to try some other random seeds. +- **FlipStreamSegMask(deprecated)**: Currently cannot use, so please use original node ComfyUI-Florence2. - **FlipStreamChat**: Loads an LLM (Large Language Model) and obtains chat responses. It only supports *.gguf files in the models/LLM folder and uses llama-cpp-python. The output format can be customized using the response_format parameter, which supports [JSON Schema](https://llama-cpp-python.readthedocs.io/en/latest/#json-schema-mode) for structured responses. - **FlipStreamParseJson**: Extracts values for multiple keys from a JSON string and joins them using a specified delimiter. +- **FlipStreamChatJson**: Mix and simplified FlipStreamChat and FlipStreamParseJson using auto generated json scheme. - **FlipStreamBatchPrompt**: A node for simple batch prompting. Use the following format for the input prompt of this node. The 'pre text' and 'append text' sections apply to all frames. The separator `----` should consist of four hyphens, and each line of 'frame text' applies evenly to the number of frames specified in 'frames'. For example, if 'frames' is 8 and there are 2 'frame text' lines, they will be applied starting from frames 0 and 4. ``` pre text, @@ -122,6 +130,14 @@ append text ``` - **FlipStreamFilmVfi**: A node for video frame interpolation. It depends on ComfyUI-Frame-Interpolation. - **FlipStreamViewer**: A node for viewing content. The 'allowip' parameter allows you to set IP addresses that can access the viewer. ComfyUI commandline options "--listen 0.0.0.0" and appropriate firewall settings are also needed for that. The 'w14exc' parameter is used to set exclude_tags for the WD14 Tagger. 'fps' can control flip speed. +- **FlipStreamViewerSimple**: Simplified FlipStreamViewer. +- **FlipStreamCurrent**: A node for setting current information text on the right panel status info. +- **FlipStreamAllowIp**: A node for setting allowip. +- **FlipStreamLoraMode**: A node for setting loraMode. +- **FlipStreamSaveApiWorkflow**: A node for save current workflow as api workflow. +- **FlipStreamRunApiWorkflow**: A node for run an api workflow. +- **FlipStreamFree**: A node for free model and comfy caches. +- **FlipStreamShutdown**: A node for scheduling shutdown (Windows only). ## For More Quality @@ -131,7 +147,7 @@ append text ## For More Speed -- ComfyUI commandline options '--mmap-torch-files --fp8_e4m3fn-text-enc --fast' are good for speed, however it depends on system and model. +- ComfyUI commandline options '--mmap-torch-files --fast' are good for speed, however it depends on system and model. - And '--use-sage-attention' option will good for RTX50xx speed up. ``` REM Before that, may be need to install Visual Studio 2022, VC++ development options and cmake. Also need to set PATH for git. @@ -147,8 +163,16 @@ copy ..\build_sage.py . python build_sage.py ``` +## For Avoid OOE + +- The ComfyUI commandline option '--reserve-vram 2' may good, it will work as buffer. +- Edit comfy code using try except pass around error points may practical for private use, but it may cause difficulty on update comfy. +- FlipStreamSaveApiWorkflow, FlipStreamRunApiWorkflow, FlipStreamSetState, FlipStreamGet nodes are useful to separate workflow to avoid OOE, because comfy keeps cache on each workflow and the cache take up large memory. + ## Workflow Examples +These workflow are old. + - **simple.json**: Customized a ComfyUI default workflow for FlipStreamViewer UI nodes. - **dmd2_lora.json**: Using DMD2 and some LoRA, only needs 5 steps to generate an image. - **segmask.json**: A 2-step process for generating frames using batch prompts and segmentation mask, followed by interpolation. @@ -161,3 +185,4 @@ python build_sage.py + From 453caa62da32b5a15cd3c551982c9e12cc8fc226 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 08:56:50 +0900 Subject: [PATCH 17/19] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71f2881..311fcad 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ python build_sage.py ## Workflow Examples -These workflow are old. +These workflow are sometimes old. - **simple.json**: Customized a ComfyUI default workflow for FlipStreamViewer UI nodes. - **dmd2_lora.json**: Using DMD2 and some LoRA, only needs 5 steps to generate an image. @@ -186,3 +186,4 @@ These workflow are old. + From dcaf4ac59388e6c0f6b7331b81dd925ad9bb8d69 Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 09:00:50 +0900 Subject: [PATCH 18/19] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 311fcad..20498f2 100644 --- a/README.md +++ b/README.md @@ -163,11 +163,11 @@ copy ..\build_sage.py . python build_sage.py ``` -## For Avoid OOE +## To Avoid OOM - The ComfyUI commandline option '--reserve-vram 2' may good, it will work as buffer. - Edit comfy code using try except pass around error points may practical for private use, but it may cause difficulty on update comfy. -- FlipStreamSaveApiWorkflow, FlipStreamRunApiWorkflow, FlipStreamSetState, FlipStreamGet nodes are useful to separate workflow to avoid OOE, because comfy keeps cache on each workflow and the cache take up large memory. +- FlipStreamSaveApiWorkflow, FlipStreamRunApiWorkflow, FlipStreamSetState, FlipStreamGet nodes are useful to separate workflow to avoid OOE, because comfy keeps cache on each workflow and the cache take up large memory. FlipStreamGet has independent cache system, it can bridge any data on workflows. ## Workflow Examples @@ -187,3 +187,4 @@ These workflow are sometimes old. + From 2bf629280c458d0cd4776fe2aadd15eced8a6a8a Mon Sep 17 00:00:00 2001 From: sakura1bgx Date: Sat, 17 Jan 2026 09:03:27 +0900 Subject: [PATCH 19/19] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 20498f2..b9bab39 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ python build_sage.py - The ComfyUI commandline option '--reserve-vram 2' may good, it will work as buffer. - Edit comfy code using try except pass around error points may practical for private use, but it may cause difficulty on update comfy. -- FlipStreamSaveApiWorkflow, FlipStreamRunApiWorkflow, FlipStreamSetState, FlipStreamGet nodes are useful to separate workflow to avoid OOE, because comfy keeps cache on each workflow and the cache take up large memory. FlipStreamGet has independent cache system, it can bridge any data on workflows. +- FlipStreamSaveApiWorkflow, FlipStreamRunApiWorkflow, FlipStreamSetState, FlipStreamGet nodes are useful to separate workflow to avoid OOE, because comfy keeps cache on each workflow and the cache take up large memory. FlipStreamGetState, FlipStreamGetParam, FlipStreamGet nodes have independent cache system, it can bridge any data on workflows. ## Workflow Examples @@ -188,3 +188,4 @@ These workflow are sometimes old. +