diff --git a/README.md b/README.md index a8fc601..b9bab39 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - Check 'Extra options', 'Auto Queue', and 'instant' on the ComfyUI control panel. - Then click the 'Queue Prompt' button. -5. The UI nodes will refresh after the workflow is completed once, so you need to click 'Update and reload' after that. +5. The UI nodes will refresh after the workflow is completed once, so you need to click 'Update' after that. 6. The 'preset' folder may be created in the current directory for saving and loading preset files. @@ -27,14 +27,13 @@ ## Optional Custom Nodes dependencies The following custom nodes can be used as needed: -- ComfyUI-WD14-Tagger: For Tagger - ComfyUI-Inspyrenet-Rembg: For FlipStreamRembg -- ComfyUI-Florence2: For FlipStreamSegMask - ComfyUI-Frame-Interpolation: For FlipStreamFilmVfi The following custom nodes may also be used within workflows: - ComfyUI Impact Pack - LoRA Tag Loader for ComfyUI +- ComfyUI-WD14-Tagger - ComfyUI-AnimateDiff-Evolved - ComfyUI-VideoHelperSuite - ComfyUI-DepthAnythingV2 @@ -44,6 +43,8 @@ The following custom nodes may also be used within workflows: llama-cpp-python for FlipStreamChat: ``` +REM Before that, may be need to install Visual Studio 2022, VC++ development options and cmake +set PATH=D:\ComfyUI_windows_portable\python_embeded;D:\ComfyUI_windows_portable\python_embeded\Scripts;%PATH% cd python_embeded python -m pip install scikit-build-core python -m pip install cmake @@ -57,11 +58,10 @@ 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` -- **Tagger**: You can capture screenshots and generate tags from images using WD14. It depends on ComfyUI-WD14-Tagger. - **Preset**: You can save and load parameters that are set on viewer controls. The 'M' button means Move to another folder. It can also be set via query: `http://localhost:8188/flipstreamviewer?showPresetDialog=1&presetFolder=folder_name&presetFile=file_name.json` - **Lora**: You can select LoRA and choose tags. It can also choose random tags. The LoRA preview box can be clicked to jump to the Civitai LoRA page if found. The 'M' button means Move to another folder. The 'T' button means Toggle. The 'R' button means Random choose. @@ -73,39 +73,52 @@ python -m pip install llama-cpp-python ## UI Nodes - **FlipStreamSection**: A section label for UI Nodes. +- **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. - **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 -- **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, @@ -117,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 @@ -126,10 +147,32 @@ append text ## For More Speed -- ComfyUI commandline options '--fast --fp8_e4m3fn-unet' 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. +REM Before that, setup the same version of comfyui python from official installer, then place include and libs in python_embeded. +REM You can get build_sage.py from https://github.com/thu-ml/SageAttention/issues/228#issuecomment-3483944852 +set PATH=D:\ComfyUI_windows_portable\python_embeded;D:\ComfyUI_windows_portable\python_embeded\Scripts;%PATH% +cd python_embeded +python -m pip install ninja +python -m pip install -U --pre triton-windows +git clone https://github.com/thu-ml/SageAttention.git +cd SageAttention +copy ..\build_sage.py . +python build_sage.py +``` + +## 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. FlipStreamGetState, FlipStreamGetParam, FlipStreamGet nodes have independent cache system, it can bridge any data on workflows. ## Workflow Examples +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. - **segmask.json**: A 2-step process for generating frames using batch prompts and segmentation mask, followed by interpolation. @@ -138,3 +181,11 @@ append text - **quick_vid2vid.json**: Quick tuning workflow for vid2vid. - **quick_vid2vid_roi.json**: An example demonstrating the use of FlipStreamGetPreviewRoi and FlipStreamGate. - **visualnobel.json**: Visual nobel like UI using FlipStreamChat. + + + + + + + + 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'] diff --git a/flipstreamviewer.py b/flipstreamviewer.py index 404d347..05d5723 100644 --- a/flipstreamviewer.py +++ b/flipstreamviewer.py @@ -2,9 +2,12 @@ import base64 import hashlib import html import io +import itertools import json import threading import time +import subprocess +import re from pathlib import Path import imageio.v3 as iio @@ -31,11 +34,6 @@ try: except: Llama = None -try: - wd14tagger = __import__("comfyui-wd14-tagger").wd14tagger -except: - wd14tagger = None - try: rembg = __import__("comfyui-inspyrenet-rembg") except: @@ -60,29 +58,32 @@ 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"] -setparam = {} -setstate = {} +refresh_updating = 0 +refresh_data = {} +refresh_param = {} 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} -frame_updating = None +state = {"presetTitle": time.strftime("%Y%m%d-%H%M"), "presetFolder": "", "presetFile": "", "loraRate": "1", "loraRank": "0", "loraMode": "", "loraFolder": "", "loraFile": "", "loraTagOptions": "[]", "loraTag": "", "loraLinkHref": "", "loraPreviewSrc": "", "darker": 0.0, "lastElapsed": 0} frame_buffer = [] frame_mtime = 0 -frame_fps = 8 -exclude_tags = "" +frame_fps = 16 +setframe_mtime = 0 +setframe_buffer = [] class AnyType(str): def __ne__(self, __value: object) -> bool: return False -any = AnyType("*") +anytype = AnyType("*") HEAD=r""" @@ -149,6 +150,11 @@ div.row { position: relative; } +div#presetXorkeyInputDiv { + display: none; + position: relative; +} + .FlipStreamSlider { width: 50%; } @@ -159,6 +165,7 @@ div.row { .FlipStreamSelectBox, .FlipStreamFolderSelect, +.FlipStreamSizeSelect, .FlipStreamFileSelect, .FlipStreamMoveFileSelect { width: 100%; @@ -168,8 +175,20 @@ div.row { display: none; } -.FlipStreamPreviewBox { +.FlipStreamPasteBox { + border: 1px dimgray; max-width: 100%; + min-height: 32px; + max-height: 160px; + object-fit: scale-down; +} + +.FlipStreamPreviewBox { + border: 1px dimgray; + max-width: 100%; + min-height: 32px; + max-height: 160px; + object-fit: scale-down; } .FlipStreamPreviewRoi { @@ -225,6 +244,7 @@ div.row { #presetFolderSelect, #movePresetSelect, +#loraModeSelect, #loraFolderSelect, #moveLoraSelect { width: 100%; @@ -235,6 +255,10 @@ div.row { display: none; } +#presetXorkey { + display: none; +} + #presetFileSelect, #presetTitleInput, #loraFileSelect, @@ -242,9 +266,7 @@ div.row { width: 80%; } -#darkerRange, -#wd14thRange, -#wd14cthRange { +#darkerRange { width: 50%; } @@ -270,7 +292,7 @@ div.row { #loraLink img { max-width: 100%; - max-height: 8em; + max-height: 160px; width: auto; height: auto; margin: auto; @@ -304,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"); @@ -314,9 +337,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); - 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 }; + const lastElapsed = document.getElementById("statusLastElapsed").value; + var res = { presetTitle: presetTitle, presetFolder: presetFolder, presetFile: presetFile, loraRate: loraRate, loraRank: loraRank, loraMode: loraMode, loraFolder: loraFolder, loraFile: loraFile, loraTagOptions: loraTagOptions, loraTag: loraTag, loraLinkHref: loraLinkHref, loraPreviewSrc: loraPreviewSrc, loraTagOptions: loraTagOptions, darker: darker, lastElapsed: lastElapsed }; document.querySelectorAll('.FlipStreamFolderSelect').forEach(x => res[x.name] = x.value); res = Object.assign(res, force_state); return res; @@ -333,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; @@ -349,15 +372,16 @@ function setParam(param) { } } -function updateParam(reload=false, force_state={}, force_param={}, search="") { - var updateButton = document.getElementById("updateButton"); - updateButton.disabled = true; +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); } @@ -367,7 +391,6 @@ function updateParam(reload=false, force_state={}, force_param={}, search="") { }).catch(error => { alert("An error occurred while updating parameter."); }); - updateButton.disabled = false; } function setupPreviewRoi() { @@ -415,9 +438,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) { @@ -425,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 => { @@ -460,6 +485,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; @@ -470,7 +496,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) { @@ -525,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 = ""; @@ -629,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'); } } @@ -667,17 +669,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 = ""; } @@ -785,6 +776,32 @@ var streamMTime = 0; var streamCache = []; var streamIndex = 0; +function detailsState() { + const ds = document.querySelectorAll('details'); + ds.forEach((d, i) => d.open = sessionStorage.getItem(`details_state-${i}`) === 'open'); + document.addEventListener('toggle', e => { + sessionStorage.setItem(`details_state-${Array.from(ds).indexOf(e.target)}`, e.target.open ? 'open' : 'closed'); + }, true); +} +detailsState(); + +function onPasteBox() { + document.querySelectorAll('.FlipStreamPasteBox').forEach(i => { + i.tabIndex = 0; + i.onclick = () => i.focus(); + i.onpaste = e => { + const f = e.clipboardData.files[0]; + if (f?.type[0] == 'i') fetch(`/flipstreamviewer/paste_upload?label=${i.name}`, {method:'POST', body:f}); + }; + i.onkeydown = e => { + if (e.key === 'Delete' || e.key === 'Backspace') { + fetch(`/flipstreamviewer/paste_remove?label=${i.name}`, {method:'POST'}); + } + }; + }); +} +onPasteBox(); + function toggleView() { const leftPanel = document.getElementById("leftPanel"); const rightPanel = document.getElementById("rightPanel"); @@ -819,6 +836,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"; @@ -844,23 +862,44 @@ var updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps); async function refreshView() { const data = await fetch("/flipstreamviewer/refresh_view") .then(r => r.json()).catch(e => ({ status: "Fails to refresh view: " + e })); + if (data.status_elapsed == 0 && document.getElementById("statusElapsed").value != 0) { + document.getElementById("statusLastElapsed").value = document.getElementById("statusElapsed").value; + } + document.getElementById("statusElapsed").value = data.status_elapsed || 0; document.getElementById("statusInfo").textContent = data.status_info || "Empty"; document.getElementById("messageBox").innerText = atob_utf8(data.message) || ""; document.getElementById("messageBox").style.fontSize = data.message_fontsize || "1rem"; - + for (const k in data.param) { + const el = document.querySelector(`[name="${k}"]`); + if (el) { + if (el.classList.contains('FlipStreamTextBox')) { + el.value = atob_utf8(data.param[k]); + } else if (el.type === 'range') { + el.value = parseFloat(data.param[k]); + } else { + el.value = data.param[k]; + } + } + } document.querySelectorAll('.FlipStreamLogBox').forEach(async x => { if (x.id in data.log) { - x.innerText = atob_utf8(data.log[x.id]) || ""; + x.value = atob_utf8(data.log[x.id]) || ""; } else { - x.innerText = ""; + x.value = ""; } }); if (streamViewFlag) { + darker = document.getElementById("darkerRange").value; document.querySelectorAll('.FlipStreamPreviewBox').forEach(async x => { if (x.src != "") { x.src = `/flipstreamviewer/preview?label=${x.name}&mtime=${data.preview_mtime[x.id] || 0}`; } }); + document.querySelectorAll('.FlipStreamPasteBox').forEach(async x => { + if (x.src != "") { + x.src = `/flipstreamviewer/paste?label=${x.name}&mtime=${data.mtime[x.name+'_mtime'] || 0}`; + } + }); const response = await fetch('/flipstreamviewer/stream/info'); streamInfo = await response.json(); @@ -881,9 +920,6 @@ async function refreshView() { updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps); } } - if (data.update_and_reload) { - updateParam(true); - } } setInterval(refreshView, 1000); """ @@ -947,7 +983,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; @@ -955,7 +990,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(); @@ -969,6 +1011,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")); } @@ -981,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; } } @@ -997,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: @@ -1026,7 +1079,10 @@ 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') + if not label: + return web.Response(status=400, text="Label required") + key = label + "PreviewBox" if key in state: data = state[key][1] @@ -1037,12 +1093,66 @@ 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() +@server.PromptServer.instance.routes.get("/flipstreamviewer/paste") +async def paste(request): + if request.remote not in allowed_ips: + raise HTTPForbidden() + + label = request.query.get('label') + if not label: + return web.Response(status=400, text="Label required") + + if label + "_thumbnail" in state: + data = state[label + "_thumbnail"] + else: + data = b"" + return web.Response(body=data, headers={"Content-Type": "image/png"}) + + +@server.PromptServer.instance.routes.post("/flipstreamviewer/paste_upload") +async def paste_upload(request): + if request.remote not in allowed_ips: + raise HTTPForbidden() + + label = request.query.get('label') + if not label: + return web.Response(status=400, text="Label required") + + data = await request.read() + img = Image.open(io.BytesIO(data)) + image = torch.from_numpy(np.array(img)).float()[None, :] / 255.0 + if image.shape[3] == 4: + image = image[:,:,:,:3] * image[:,:,:,3:4] + state[label] = image + state[label + "_thumbnail"] = data + state[label + "_mtime"] = time.time() + return web.Response() + + +@server.PromptServer.instance.routes.post("/flipstreamviewer/paste_remove") +async def paste_upload(request): + if request.remote not in allowed_ips: + raise HTTPForbidden() + + label = request.query.get('label') + if not label: + return web.Response(status=400, text="Label required") + + if label in state: + del state[label] + del state[label + "_thumbnail"] + del state[label + "_mtime"] + return web.Response() + + @server.PromptServer.instance.routes.get("/flipstreamviewer") async def viewer(request): if request.remote not in allowed_ips: @@ -1051,11 +1161,25 @@ async def viewer(request): block = {} - def add_section(title, section, hook): + def add_section(title, section, **_): block[f"{title}_{section}"] = f""" -
{section}
""" + +
+ {section}""" - def add_slider(title, label, default, min, max, step): + def add_button(title, run, capture, **_): + block[f"{title}"] = f""" +
""" + if run: + block[f"{title}"] += f""" + """ + if capture: + block[f"{title}"] += f""" + """ + block[f"{title}"] += f""" +
""" + + def add_slider(title, label, default, min, max, step, **_): if not label.isidentifier(): raise RuntimeError(f"{title}: label must contain only valid identifier characters.") param.setdefault(label, default) @@ -1065,14 +1189,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) @@ -1080,22 +1204,22 @@ 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, 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.") @@ -1104,7 +1228,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_sizeselect(title, label, listitems, **_): + listitems = listitems.split(",") + if not label.isidentifier(): + raise RuntimeError(f"{title}: label must contain only valid identifier characters.") + if not all(item == html.escape(item) for item in listitems): + raise RuntimeError(f"{title}: listitems must contain only HTML-acceptable characters.") + param.setdefault(label, "") + text_html = f""" + """ + block[f"{title}_{label}"] = text_html + + def add_fileselect(title, label, folder_name, folder_path, mode, use_sub, use_move, **_): if not label.isidentifier(): raise RuntimeError(f"{title}: label must contain only valid identifier characters.") if not (mode == "" or mode.isidentifier()): @@ -1133,7 +1274,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: @@ -1185,22 +1326,32 @@ async def viewer(request):
""" - def add_logbox(title, label, log, rows): + def add_pastebox(title, label, **_): + if not label.isidentifier(): + raise RuntimeError(f"{title}: label must contain only valid identifier characters.") + block[f"{title}_{label}"] = f""" +
+ {label} +
""" + + def add_logbox(title, label, rows, **_): if not label.isidentifier(): raise RuntimeError(f"{title}: label must contain only valid identifier characters.") if (label + "LogBox") in state: block[f"{title}_{label}"] = f""" """ - 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"] 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": @@ -1209,10 +1360,14 @@ 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": add_previewbox(title, **inputs) + if class_type == "FlipStreamPasteBox": + add_pastebox(title, **inputs) if class_type == "FlipStreamLogBox": add_logbox(title, **inputs) @@ -1220,35 +1375,29 @@ async def viewer(request):
- +
+
+ Input {"".join([x[1] for x in sorted(block.items())])} +
-
Status
- -
Darker
+ + +
+ Status +
{state["darker"]}drk
-
Tagger
-
- - -
-
- - {state["wd14th"]}wth -
-
- - {state["wd14cth"]}cth -
-
Preset
+
+
+ Preset
+
+ +
-
Lora
+
+
+ Lora +
- + - +
@@ -1343,6 +1501,7 @@ async def viewer(request):
+
@@ -1428,29 +1587,41 @@ 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_info.append(f"updating {int(time.time() - frame_updating)}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] - state.update(setstate) - setstate.clear() - data = {} + data = refresh_data.copy() + data["param"] = refresh_param.copy() + param.update(refresh_param) + refresh_param.clear() + data["status_elapsed"] = status_elapsed data["status_info"] = status_info data["preview_mtime"] = {key: state[key][0] for key in state if key.endswith("PreviewBox")} + data["mtime"] = {key: state[key] for key in state if key.endswith("_mtime")} data["log"] = {key: state[key] for key in state if key.endswith("LogBox")} - 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) @@ -1461,11 +1632,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() @@ -1501,22 +1668,6 @@ async def get_lorainfo(request): return web.json_response(lorainfo) -@server.PromptServer.instance.routes.post("/flipstreamviewer/get_wd14tag") -async def get_wd14tag(request): - if request.remote not in allowed_ips: - raise HTTPForbidden() - - if wd14tagger is None: - raise RuntimeError("get_wd14tag: ComfyUI-WD14-Tagger must be installed to use this function.") - - stt = await request.json() - state.update(stt); - tags = [] - if frame_buffer: - tags = await wd14tagger.tag(Image.open(io.BytesIO(frame_buffer[0])), "wd-v1-4-moat-tagger-v2.onnx", state["wd14th"], state["wd14cth"], exclude_tags) - return web.json_response({"tags": tags}) - - @server.PromptServer.instance.routes.post("/flipstreamviewer/set_frame") async def set_frame(request): if request.remote not in allowed_ips: @@ -1524,9 +1675,13 @@ async def set_frame(request): frame = base64.b64decode((await request.text()).split(',', 1)[1]) global frame_buffer - frame_buffer = [frame] 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() @@ -1572,14 +1727,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", "")} @@ -1597,12 +1769,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() @@ -1614,15 +1792,36 @@ 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): + def run(self, hook=True, **_): + return (hook,) + + +class FlipStreamButton: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "run": ("BOOLEAN", {"default": True}), + "capture": ("BOOLEAN", {"default": True}), + }, + "optional": { + "hook": (anytype,), + } + } + + RETURN_TYPES = (anytype,) + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + def run(self, hook=True, **_): return (hook,) @@ -1640,17 +1839,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): - global frame_updating - frame_updating = time.time() + def run(self, label, default, **_): param.setdefault(label, default) return (float(param[label]), int(float(param[label])), bool(float(param[label]))) @@ -1666,20 +1864,20 @@ 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): - global frame_updating - frame_updating = time.time() + return hash(param[label]) + + def run(self, label, default, **_): param.setdefault(label, btoa_utf8(default)) - return (atob_utf8(param[label]),) + text = atob_utf8(param[label]) + return (text, bool(text)) class FlipStreamInputBox: @@ -1694,27 +1892,24 @@ 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): - param.setdefault(label, default) - return hash((param[label],)) - - def floator0(self, v): - try: - return float(v) - except: - return 0 + def IS_CHANGED(cls, label, default, **_): + return hash(param.get(label)) - def run(self, label, default, boxtype, **kwargs): - global frame_updating - frame_updating = time.time() + def run(self, label, default, boxtype, **_): 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: @@ -1730,19 +1925,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): - global frame_updating - frame_updating = time.time() + def run(self, label, default, **_): param.setdefault(label, "") item = param[label] if param[label] else default return (item, param[label] != "") @@ -1756,13 +1949,75 @@ class FlipStreamSelectBox_Scheduler(FlipStreamSelectBox): LISTITEMS = comfy.samplers.KSampler.SCHEDULERS +class FlipStreamSizeSelect: + SIZEDICT = { + "sdxl": { + "1:1": (1024, 1024), + "16:9": (1344, 768), + "9:16": (768, 1344), + "4:3": (1152, 896), + "3:4": (896, 1152), + "3:2": (1216, 832), + "2:3": (832, 1216), + }, + "qwen": { + "1:1": (1328, 1328), + "16:9": (1664, 928), + "9:16": (928, 1664), + "4:3": (1472, 1104), + "3:4": (1104, 1472), + "3:2": (1584, 1056), + "2:3": (1056, 1584), + }, + "wan": { + "1:1": (512, 512), + "16:9": (832, 480), + "9:16": (480, 832), + "4:3": (832, 480), + "3:4": (480, 832), + "3:2": (832, 480), + "2:3": (480, 832), + } + } + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "label": ("STRING", {"default": "empty"}), + "default": (list(s.SIZEDICT["sdxl"].keys()),), + "modeltype": (["sdxl", "qwen", "wan"],), + "listitems": ("STRING", {"default": ",".join(s.SIZEDICT["sdxl"].keys())}) + }, + } + + RETURN_TYPES = ("INT", "INT", "BOOLEAN") + RETURN_NAMES = ("width", "height", "enable") + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + @classmethod + def IS_CHANGED(cls, label, default, **_): + param.setdefault(label, default) + return hash(param[label]) + + def run(self, label, default, modeltype, **_): + param.setdefault(label, default) + width, height = self.SIZEDICT[modeltype][param.get(label)] + return (width, height, param[label] != "") + + +class FlipStreamGetSize(FlipStreamSizeSelect): + pass + + class FlipStreamFileSelect: FOLDER_NAME = "" FOLDER_PATH = "" @staticmethod - def get_filelist(folder_name, folder_path): - if folder_name == "checkpoints": + def get_filelist(folder_name, folder_path, mode): + if folder_name == "checkpoints" and not mode: return CheckpointLoaderSimple.INPUT_TYPES()["required"]["ckpt_name"][0] elif folder_name == "vae": return VAELoader.INPUT_TYPES()["required"]["vae_name"][0] @@ -1779,14 +2034,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": ""}), @@ -1795,19 +2050,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): - global frame_updating - frame_updating = time.time() + def run(self, label, default, folder_path, **_): param.setdefault(label, "") file = param[label] if param[label] else default return (file, str(Path(folder_path, file)), param[label] != "") @@ -1818,11 +2071,21 @@ 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() +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() @@ -1869,7 +2132,9 @@ 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) image = Image.fromarray(buf) image.thumbnail((256, 256)) @@ -1879,6 +2144,30 @@ 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 hash(state.get(label + "_mtime")) + + def run(self, label, **_): + if label not in state: + return (None, False) + return (state[label], True) + + class FlipStreamLogBox: @classmethod def INPUT_TYPES(s): @@ -1895,31 +2184,31 @@ class FlipStreamLogBox: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, label, log, **kwargs): + def run(self, label, log, **_): state[label + "LogBox"] = btoa_utf8(log) 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": (any,), + "hook": (anytype,), } } - RETURN_TYPES = (any,) + RETURN_TYPES = (anytype,) OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, delay_sec, hook=None): - time.sleep(delay_sec) - setstate["update_and_reload"] = True + def run(self, hook=True, **_): + server.PromptServer.instance.send_sync("FlipStreamViewer_run_once", {}) + global refresh_updating + refresh_updating = 0 return (hook,) @@ -1932,21 +2221,139 @@ 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): - setstate["message"] = btoa_utf8(message) - setstate["message_fontsize"] = f"{fontsize}rem" + def run(self, message, fontsize, hook=True, **_): + refresh_data["message"] = btoa_utf8(message) + refresh_data["message_fontsize"] = f"{fontsize}rem" return (hook,) +class FlipStreamAnd: + @classmethod + def INPUT_TYPES(s): + return { + "required": {}, + "optional": {f"v{i}": (anytype,) for i in range(10)} + } + + RETURN_TYPES = (anytype,) + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + def run(self, **kwargs): + res = None + for res in kwargs.values(): + if not res: break + return (res,) + + +class FlipStreamOr: + @classmethod + def INPUT_TYPES(s): + return { + "required": {}, + "optional": {f"v{i}": (anytype,) for i in range(10)} + } + + RETURN_TYPES = (anytype,) + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + def run(self, **kwargs): + res = None + for res in kwargs.values(): + if res: break + return (res,) + + +class FlipStreamSetState: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "label": ("STRING", {"default": "empty"}), + "replace": ("BOOLEAN", {"default": False}), + "thumbnail": ("BOOLEAN", {"default": False}), + }, + "optional": { + "hook": (anytype,), + "value": (anytype,), + } + } + + RETURN_TYPES = (anytype,) + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + def run(self, label, replace, thumbnail, hook=True, value=None, **_): + if replace or label not in state: + if thumbnail and value is not None: + buf = np.array(value[0].cpu().numpy() * 255, dtype=np.uint8) + image = Image.fromarray(buf) + image.thumbnail((256, 256)) + with io.BytesIO() as output: + image.save(output, format="PNG", compress_level=STREAM_COMPRESSION) + state[label + "_thumbnail"] = output.getvalue() + state[label + "_mtime"] = time.time() + else: + if label + "_thumbnail" in state: + del state[label + "_thumbnail"] + del state[label + "_mtime"] + state[label] = value + return (hook,) + + +class FlipStreamGetState: + CACHED_LASTGET = {} + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "label": ("STRING", {"default": "empty"}), + }, + "optional": { + "hook": (anytype,), + }, + "hidden": { + "unique_id": "UNIQUE_ID" + } + } + + RETURN_TYPES = (anytype, "BOOLEAN", "BOOLEAN") + RETURN_NAMES = ("value", "enable", "changed") + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + @classmethod + def IS_CHANGED(cls, label, **_): + value = state.get(label) + return hash(value[:1024] if hasattr(value, '__getitem__') else value) + + def run(self, label, unique_id, **_): + enable = label in param + value = state.get(label) + + changed = False + h = hash(str(value[:1024]) if hasattr(value, '__getitem__') else value) + cache = FlipStreamGetState.CACHED_LASTGET.setdefault(unique_id, {}) + if cache.get(label, h) != h: + changed = True + cache[label] = h + return (value, enable, changed) + + class FlipStreamSetParam: @classmethod def INPUT_TYPES(s): @@ -1958,26 +2365,29 @@ 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, hook=True, **_): empty = "" if b64enc: value = btoa_utf8(value) empty = btoa_utf8(empty) if replace or label not in param or param[label] == empty: - setparam[label] = value + param[label] = value + refresh_param[label] = value return (hook,) class FlipStreamGetParam: + CACHED_LASTGET = {} + @classmethod def INPUT_TYPES(s): return { @@ -1986,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: @@ -2033,13 +2527,11 @@ 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) 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 @@ -2079,7 +2571,7 @@ class FlipStreamTextReplace: "replace": ("STRING", {"default": ""}), }, "optional": { - "value": (any,), + "value": (anytype,), } } @@ -2096,6 +2588,76 @@ class FlipStreamTextReplace: return (text,) +class FlipStreamTextConcat: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "joinstr": ("STRING", {"default": "", "multiline": True}), + "enable": ("BOOLEAN", {"default": True}) + }, + "optional": { + **{f"text{i}": ("STRING", {"default": ""}) for i in range(20)} + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + def run(self, joinstr, enable, **kwargs): + if not enable: + return ("",) + + text = [] + for k in kwargs: + if k.startswith("text"): + val = kwargs[k] + text.append(val) + + return (str(joinstr).join(map(str, filter(None, text))),) + + +class FlipStreamGetFrame: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "index": ("INT", {"default": 0, "min": 0}), + "frames": ("INT", {"default": 1, "min": 1}), + "capture_only": ("BOOLEAN", {"default": True}), + "enable": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("IMAGE", "BOOLEAN") + RETURN_NAMES = ("image", "enable") + FUNCTION = "run" + CATEGORY = "FlipStreamViewer" + + @classmethod + def IS_CHANGED(cls, capture_only, **_): + if capture_only: + return setframe_mtime + else: + return frame_mtime + + def run(self, index, frames, capture_only, enable, **_): + if not enable: + return (None, False) + buf = setframe_buffer if capture_only else frame_buffer + images_list = [ + np.array(Image.open(io.BytesIO(buf[i]))) + for i in range(index, min(index + frames, len(buf))) + ] + if not images_list: + return (None, False) + image = torch.from_numpy(np.stack(images_list)).float() / 255.0 + if image is not None and image.shape[3] == 4: + image = image[:,:,:,:3] * image[:,:,:,3:4] + return (image, True) + + class FlipStreamScreenGrabber: @classmethod def INPUT_TYPES(s): @@ -2184,7 +2746,7 @@ class FlipStreamVideoInput: def run(self, path, first, step, frames): if not path or not Path(path).is_file(): - return (torch.zeros([1, 32, 32, 3]), False) + return (torch.zeros((1, 64, 64, 3)), False) with iio.imopen(path, "r") as file: buf = [file.read(index=i) for i in range(first, first+(frames-1)*step+1, step)] buf = np.stack(buf).astype(np.float32) / 255 @@ -2198,9 +2760,11 @@ 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}), + "strim": ("INT", {"default": 0, "min": 0}), + "etrim": ("INT", {"default": 0, "min": 0}), }, "optional": { "image": ("IMAGE",), @@ -2208,30 +2772,47 @@ 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 - if image is not None and image.shape[3] == 4: - image = image[:,:,:,:3] * image[:,:,:,3:4] - if image is not None and image.shape[0] >= frames: - buf = image[:frames] + image = None + if image is not None: + if image.shape[3] == 4: + image = image[:,:,:,:3] * image[:,:,:,3:4] + if height and not width: + width = int(image.shape[2] * height / image.shape[1] // 32 * 32) + if width and not height: + height = int(image.shape[1] * width / image.shape[2] // 32 * 32) + if not width and not height: + width = image.shape[2] + height = image.shape[1] + if not frames: + frames = image.shape[0] + if image is not None and image.shape[0] >= frames and frames > strim + etrim: + buf = image[strim:frames-etrim] buf = buf.movedim(-1,1) - buf = comfy.utils.common_upscale(buf, width, height, "lanczos", "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 + 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,) + return (image, latent, False) class FlipStreamSwitch: @@ -2239,15 +2820,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" @@ -2314,18 +2895,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" @@ -2334,7 +2915,7 @@ class FlipStreamGate: return (model, pos, neg, latent, a, b, c, d, e, f, g, h) -class FlipStreamRembg: +class FlipStreamRembg: @classmethod def INPUT_TYPES(s): return { @@ -2404,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', @@ -2455,13 +3036,15 @@ class FlipStreamSegMask: class FlipStreamChat: + CACHED_OUTPUT = {} + @classmethod def INPUT_TYPES(cls): return { "required": { "model_file": ([path.name for path in Path(folder_paths.models_dir, "LLM").glob("*.gguf")],), - "n_ctx": ("INT", {"default": 2048}), - "n_gpu_layers": ("INT", {"default": -1}), + "n_ctx": ("INT", {"default": 2048, "min": 0, "max": 8192}), + "n_gpu_layers": ("INT", {"default": -1, "min": -1}), "unload_other_models": ("BOOLEAN", {"default": False}), "close_after_use": ("BOOLEAN", {"default": False}), "system": ("STRING", {"default": "", "multiline": True}), @@ -2472,29 +3055,33 @@ 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}), - "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) @@ -2502,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: @@ -2526,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: @@ -2568,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"): @@ -2586,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): @@ -2693,20 +3357,17 @@ class FlipStreamViewer: "required": { "tensor": ("IMAGE",), "allowip": ("STRING", {"default": ""}), - "wd14exc": ("STRING", {"default": ""}), "idle": ("FLOAT", {"default": 1.0, "min": 0.0}), - "fps": ("INT", {"default": 8, "min": 1, "max": 30}), + "fps": ("INT", {"default": 16, "min": 1, "max": 30}), "loramode": ("STRING", {"default": ""}), - "reset_updating": ("BOOLEAN", {"default": True}), + "pingpong": ("BOOLEAN", {"default": True}), }, } @classmethod - def IS_CHANGED(cls, allowip, wd14exc, idle, loramode, **kwargs): + def IS_CHANGED(cls, allowip, idle, loramode, **_): global allowed_ips - global exclude_tags allowed_ips = ["127.0.0.1"] + list(map(str.strip, allowip.split(","))) - exclude_tags = wd14exc state["loraMode"] = loramode time.sleep(idle) return None @@ -2716,10 +3377,14 @@ class FlipStreamViewer: FUNCTION = "run" CATEGORY = "FlipStreamViewer" - def run(self, tensor, fps, reset_updating, **kwargs): + 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: + 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: @@ -2727,40 +3392,304 @@ 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, "FlipStreamSlider": FlipStreamSlider, "FlipStreamTextBox": FlipStreamTextBox, "FlipStreamInputBox": FlipStreamInputBox, "FlipStreamSelectBox_Samplers": FlipStreamSelectBox_Samplers, "FlipStreamSelectBox_Scheduler": FlipStreamSelectBox_Scheduler, + "FlipStreamSizeSelect": FlipStreamSizeSelect, + "FlipStreamGetSize": FlipStreamGetSize, "FlipStreamFileSelect_Checkpoints": FlipStreamFileSelect_Checkpoints, + "FlipStreamFileSelect_Loras": FlipStreamFileSelect_Loras, "FlipStreamFileSelect_VAE": FlipStreamFileSelect_VAE, + "FlipStreamFileSelect_LLM": FlipStreamFileSelect_LLM, "FlipStreamFileSelect_ControlNetModel": FlipStreamFileSelect_ControlNetModel, "FlipStreamFileSelect_TensorRT": FlipStreamFileSelect_TensorRT, "FlipStreamFileSelect_AnimateDiffModel": FlipStreamFileSelect_AnimateDiffModel, "FlipStreamFileSelect_Input": FlipStreamFileSelect_Input, "FlipStreamFileSelect_Output": FlipStreamFileSelect_Output, "FlipStreamPreviewBox": FlipStreamPreviewBox, + "FlipStreamPasteBox": FlipStreamPasteBox, "FlipStreamLogBox": FlipStreamLogBox, - "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, "FlipStreamTextReplace": FlipStreamTextReplace, + "FlipStreamTextConcat": FlipStreamTextConcat, "FlipStreamScreenGrabber": FlipStreamScreenGrabber, "FlipStreamVideoInput": FlipStreamVideoInput, "FlipStreamSource": FlipStreamSource, @@ -2772,46 +3701,79 @@ 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 = { "FlipStreamSection": "FlipStreamSection", + "FlipStreamButton": "FlipStreamButton", "FlipStreamSlider": "FlipStreamSlider", "FlipStreamTextBox": "FlipStreamTextBox", "FlipStreamInputBox": "FlipStreamInputBox", "FlipStreamSelectBox_Samplers": "FlipStreamSelectBox_Samplers", "FlipStreamSelectBox_Scheduler": "FlipStreamSelectBox_Scheduler", + "FlipStreamSizeSelect": "FlipStreamSizeSelect", + "FlipStreamGetSize": "FlipStreamGetSize", "FlipStreamFileSelect_Checkpoints": "FlipStreamFileSelect_Checkpoints", + "FlipStreamFileSelect_Loras": "FlipStreamFileSelect_Loras", "FlipStreamFileSelect_VAE": "FlipStreamFileSelect_VAE", + "FlipStreamFileSelect_LLM": "FlipStreamFileSelect_LLM", "FlipStreamFileSelect_ControlNetModel": "FlipStreamFileSelect_ControlNetModel", "FlipStreamFileSelect_TensorRT": "FlipStreamFileSelect_TensorRT", "FlipStreamFileSelect_AnimateDiffModel": "FlipStreamFileSelect_AnimateDiffModel", "FlipStreamFileSelect_Input": "FlipStreamFileSelect_Input", "FlipStreamFileSelect_Output": "FlipStreamFileSelect_Output", "FlipStreamPreviewBox": "FlipStreamPreviewBox", + "FlipStreamPasteBox": "FlipStreamPasteBox", "FlipStreamLogBox": "FlipStreamLogBox", - "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", "FlipStreamTextReplace": "FlipStreamTextReplace", + "FlipStreamTextConcat": "FlipStreamTextConcat", "FlipStreamScreenGrabber": "FlipStreamScreenGrabber", "FlipStreamVideoInput": "FlipStreamVideoInput", "FlipStreamSource": "FlipStreamSource", "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", } 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);}); + }; + } +});