Compare commits

...
10 Commits
Author SHA1 Message Date
sakura1bgx 0bb1fe65f5 Update pyproject.toml 2025-09-20 22:46:09 +09:00
sakura1bgx 1ae9d32ed9 Update requirements.txt 2025-09-20 22:45:48 +09:00
sakura1bgx 8a2eee3b2a Update flipstreamviewer.py
- Change streaming method to improve processing speed.
2025-09-20 22:45:07 +09:00
sakura1bgx 927f47d73b Update pyproject.toml 2025-09-05 19:53:00 +09:00
sakura1bgx 8b70e94d0c Update flipstreamviewer.py
- FlipStreamTextReplace: 'find' support multiple words by comma(,) separated.
- FlipStreamParseJson: igonre json error.
- FlipStreamViewer: show frame size console log
2025-09-05 19:52:06 +09:00
sakura1bgx 675ab1f3b6 Update README.md 2025-07-05 10:32:49 +09:00
sakura1bgx 38c85016cd Update README.md 2025-07-05 10:23:39 +09:00
sakura1bgx c5d89b6f5a Update pyproject.toml 2025-07-05 10:10:04 +09:00
sakura1bgx 9d08101663 Update README.md 2025-07-05 10:08:51 +09:00
sakura1bgx 6b1f89a229 Update flipstreamviewer.py
- Align MessageBox layout
- Add FlipStreamLogBox
- Clear old params when loading presets
- Add response_format to FlipStreamChat
- Change max_history behavior in FlipStreamChat
- Add FlipStreamParseJson
2025-07-05 09:55:43 +09:00
4 changed files with 232 additions and 93 deletions
+6 -4
View File
@@ -42,7 +42,7 @@ The following custom nodes may also be used within workflows:
## Optional Python Packages Dependencies
llama-cpp-python for FlipStreamLoadChatModel:
llama-cpp-python for FlipStreamChat:
```
cd python_embeded
python -m pip install scikit-build-core
@@ -65,9 +65,9 @@ python -m pip install llama-cpp-python
- **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.
- **Toggle View**: You can click the center panel to hide the left/right panels and the center message box, a subsequent click will show them. It can also be set via query:
- **Toggle View**: You can click the center panel to hide the left/right panels and the message box, a subsequent click will show them. It can also be set via query:
`http://localhost:8188/flipstreamviewer?toggleView=1`
- **Message Box**: If a message is set by FlipStreamSetMessage, you can view it at the bottom of the center panel.
- **Message Box**: If a message is set by FlipStreamSetMessage, you can view it at the top left of the center panel.
- **Auto Hide**: Stream and preview in the viewer will automatically hide after 5 minutes if the page is not reloaded.
## UI Nodes
@@ -86,6 +86,7 @@ python -m pip install llama-cpp-python
- **FlipStreamFileSelect_Input**: A file selector for ComfyUI input folder.
- **FlipStreamFileSelect_Output**: A file selector for ComfyUI output folder.
- **FlipStreamPreviewBox**: A box for previewing the input image. You can select the ROI (Region of Interest) in the preview by dragging the mouse.
- **FlipStreamLogBox**: A box for displaying logs on the screen.
## Other Nodes
@@ -103,7 +104,8 @@ python -m pip install llama-cpp-python
- **FlipStreamGate**: 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.
- **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.
- **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.
- **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,
+225 -87
View File
@@ -1,13 +1,12 @@
import base64
import hashlib
import html
import io
import json
import threading
import time
from pathlib import Path
from io import BytesIO
import imageio.v3 as iio
import requests
import torch
import torch.nn.functional as NNF
@@ -60,18 +59,22 @@ def btoa_utf8(value):
return base64.b64encode(value.encode("utf-8")).decode()
def atob_utf8(value):
if not value:
return ""
return base64.b64decode(value.encode()).decode("utf-8")
STREAM_COMPRESSION = 0
STREAM_COMPRESSION = 1
UPDATE_DELAY = 1.0
allowed_ips = ["127.0.0.1"]
setparam = {}
setstate = {}
param = {"lora": "", "_capture_offsetX": 0, "_capture_offsetY": 0, "_capture_scale": 100}
state = {"message": btoa_utf8(""), "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}
default_param = {"lora": "", "_capture_offsetX": 0, "_capture_offsetY": 0, "_capture_scale": 100}
param = default_param.copy()
state = {"message": "", "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
frame_buffer = None
frame_buffer = []
frame_mtime = 0
frame_fps = 8
exclude_tags = ""
class AnyType(str):
@@ -91,8 +94,6 @@ html {
body {
color: lightgray;
background: url('/flipstreamviewer/stream') rgba(0, 0, 0, 0) no-repeat top center local;
background-blend-mode: overlay;
margin: 4px;
padding: 0px;
}
@@ -211,6 +212,9 @@ div.row {
#mainDialog {
display: flex;
background-position: center;
background-repeat: no-repeat;
background-blend-mode: overlay;
}
#captureDialog, #tagDialog, #presetDialog {
@@ -243,18 +247,14 @@ div.row {
width: 50%;
}
#toggleViewButton {
width: 100%;
height: 85%;
border: none;
background: transparent;
}
#messageBox {
width: 100%;
height: 15%;
height: 100%;
border: none;
padding: 10px;
background: transparent;
text-align: left;
user-select: none;
font-size: 1.5rem;
text-shadow:
black 2px 0px, black -2px 0px,
@@ -293,6 +293,9 @@ function btoa_utf8(str) {
}
function atob_utf8(str) {
if (!str) {
return "";
}
return decodeURIComponent(escape(atob(str)));
}
@@ -775,6 +778,12 @@ function closeTagDialog() {
SCRIPT_VIEW=r"""
var toggleViewFlag = true;
var streamViewFlag = true;
var streamInfo = { mtime: 0, fps: 8, count: 0 };
var streamMTime = 0;
var streamCache = [];
var streamIndex = 0;
function toggleView() {
const leftPanel = document.getElementById("leftPanel");
const rightPanel = document.getElementById("rightPanel");
@@ -800,35 +809,74 @@ function toggleView() {
function hideView() {
const leftPanel = document.getElementById("leftPanel");
const centerPanel = document.getElementById("centerPanel");
const rightPanel = document.getElementById("rightPanel");
const presetLeftPanel = document.getElementById("presetLeftPanel");
const presetRightPanel = document.getElementById("presetRightPanel");
const messageBox = document.getElementById("messageBox");
closeCaptureDialog();
document.body.style.backgroundImage = "none";
messageBox.style.visibility = "hidden";
document.getElementById("loraPreview").src = "";
document.querySelectorAll('.FlipStreamPreviewBox').forEach(x => x.src = "");
leftPanel.style.visibility = "hidden";
centerPanel.style.visibility = "hidden";
rightPanel.style.visibility = "hidden";
presetLeftPanel.style.visibility = "hidden";
presetRightPanel.style.visibility = "hidden";
toggleViewFlag = false;
streamViewFlag = false;
}
setTimeout(hideView, 300 * 1000);
async function updateStreamView() {
const container = document.getElementById('mainDialog');
if (streamViewFlag && streamCache.length > 0) {
const img = streamCache[streamIndex];
container.style.backgroundImage = `url(${img.src})`;
streamIndex = (streamIndex + 1) % streamCache.length;
} else {
container.style.backgroundImage = 'none';
}
}
var updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps);
async function refreshView() {
const data = await fetch("/flipstreamviewer/refresh_view")
.then(r => r.json()).catch(e => ({ status: "Fails to refresh view: " + e }));
document.getElementById("statusInfo").textContent = data.status_info || "Empty";
document.getElementById("messageBox").textContent = atob_utf8(data.message) || "";
if (document.body.style.backgroundImage != "none") {
document.body.style.backgroundImage = `url('/flipstreamviewer/stream?mtime=${data.stream_mtime || 0}')`;
document.getElementById("messageBox").innerText = atob_utf8(data.message) || "";
document.querySelectorAll('.FlipStreamLogBox').forEach(async x => {
if (x.id in data.log) {
x.innerText = atob_utf8(data.log[x.id]) || "";
} else {
x.innerText = "";
}
});
if (streamViewFlag) {
document.querySelectorAll('.FlipStreamPreviewBox').forEach(async x => {
if (x.src != "") {
x.src = `/flipstreamviewer/preview?label=${x.name}&mtime=${data.preview_mtime[x.id] || 0}`;
}
});
const response = await fetch('/flipstreamviewer/stream/info');
streamInfo = await response.json();
if (streamInfo.mtime != streamMTime) {
streamMTime = streamInfo.mtime;
clearInterval(updateStreamInterval);
streamIndex = 0;
streamCache = await Promise.all(
Array.from({ length: streamInfo.count }, (_, j) =>
new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => resolve(img);
img.src = `/flipstreamviewer/stream/${j}.png?mtime=${streamInfo.mtime}`;
})
)
);
updateStreamInterval = setInterval(updateStreamView, 1000 / streamInfo.fps);
}
}
if (data.update_and_reload) {
updateParam(true);
@@ -840,25 +888,21 @@ setInterval(refreshView, 1000);
SCRIPT_CAPTURE=r"""
let capturedCanvas = document.createElement("canvas");
async function capture() {
try {
const video = document.createElement("video");
video.srcObject = await navigator.mediaDevices.getDisplayMedia({ video: true });
video.style.display = "none";
video.play();
video.addEventListener("loadedmetadata", () => {
const { videoWidth, videoHeight } = video;
capturedCtx = capturedCanvas.getContext("2d");
capturedCtx.clearRect(0, 0, capturedCanvas.width, capturedCanvas.height);
capturedCanvas.width = videoWidth;
capturedCanvas.height = videoHeight;
capturedCtx.drawImage(video, 0, 0);
video.srcObject.getTracks().forEach(track => track.stop());
video.srcObject = null;
updateCanvas();
});
} catch (error) {
alert("An error occurred while capture.");
}
const video = document.createElement("video");
video.srcObject = await navigator.mediaDevices.getDisplayMedia({ video: true });
video.style.display = "none";
video.play();
video.addEventListener("loadedmetadata", () => {
const { videoWidth, videoHeight } = video;
capturedCtx = capturedCanvas.getContext("2d");
capturedCtx.clearRect(0, 0, capturedCanvas.width, capturedCanvas.height);
capturedCanvas.width = videoWidth;
capturedCanvas.height = videoHeight;
capturedCtx.drawImage(video, 0, 0);
video.srcObject.getTracks().forEach(track => track.stop());
video.srcObject = null;
updateCanvas();
});
}
function updateCanvas() {
@@ -900,6 +944,7 @@ 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;
@@ -907,7 +952,7 @@ function onInputDarker(value=-1) {
document.getElementById("darkerRange").value = value;
}
document.getElementById("darkerValue").innerText = value;
document.body.style.backgroundColor = 'rgba(0,0,0,' + value + ')';
container.style.backgroundColor = 'rgba(0,0,0,' + value + ')';
}
onInputDarker();
@@ -948,12 +993,29 @@ function parseQueryParam() {
parseQueryParam();
"""
@server.PromptServer.instance.routes.get("/flipstreamviewer/stream")
async def stream(request):
@server.PromptServer.instance.routes.get("/flipstreamviewer/stream/info")
async def stream_count(request):
if request.remote not in allowed_ips:
raise HTTPForbidden()
return web.Response(body=frame_buffer, headers={"Content-Type": "image/apng"})
return web.json_response({"mtime": frame_mtime, "fps": frame_fps, "count": len(frame_buffer)})
@server.PromptServer.instance.routes.get("/flipstreamviewer/stream/{frame_id:\\d+}.png")
async def stream_frame(request):
if request.remote not in allowed_ips:
raise HTTPForbidden()
try:
frame_id = int(request.match_info["frame_id"])
except (KeyError, ValueError):
raise web.HTTPBadRequest(text="Invalid frame ID")
if not (0 <= frame_id < len(frame_buffer)):
raise web.HTTPNotFound(text="Frame not found")
return web.Response(body=frame_buffer[frame_id], headers={"Content-Type": "image/png"})
@server.PromptServer.instance.routes.get("/flipstreamviewer/preview")
@@ -1003,7 +1065,7 @@ async def viewer(request):
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, default)
param.setdefault(label, btoa_utf8(default))
block[f"{title}_{label}"] = f"""
<textarea class="FlipStreamTextBox" id="{label}TextBox" style="color: lightslategray;" placeholder="{label}" rows="{rows}" name="{label}">{atob_utf8(param[label])}</textarea>"""
@@ -1119,6 +1181,13 @@ async def viewer(request):
<img class="FlipStreamPreviewBox" id="{label}PreviewBox" name="{label}" src="/flipstreamviewer/preview?label={label}" alt onerror="this.onerror = null; this.src='';" />
<canvas class="FlipStreamPreviewRoi" id="{label}PreviewRoi"></canvas>
</div>"""
def add_logbox(title, label, log, rows):
if not label.isidentifier():
raise RuntimeError(f"{title}: label must contain only valid identifier characters.")
if (label + "LogBox") in state:
block[f"{title}_{label}"] = f"""
<textarea class="FlipStreamLogBox" id="{label}LogBox" style="color: lightslategray;" placeholder="{label}" rows="{rows}" name="{label}" readonly></textarea>"""
hist = server.PromptServer.instance.prompt_queue.get_history(max_items=1)
nodedict = next(iter(hist.values()))["prompt"][2] if hist else None
@@ -1141,6 +1210,8 @@ async def viewer(request):
add_fileselect(title, **inputs)
if class_type == "FlipStreamPreviewBox":
add_previewbox(title, **inputs)
if class_type == "FlipStreamLogBox":
add_logbox(title, **inputs)
text_html = f"""<html>{HEAD}<body>
<div id="mainDialog">
@@ -1150,9 +1221,8 @@ async def viewer(request):
</div>
{"".join([x[1] for x in sorted(block.items())])}
</div>
<div id="centerPanel">
<button id="toggleViewButton" onclick="toggleView()"></button>
<div id="messageBox" onclick="toggleView()"></div>
<div id="centerPanel" onclick="toggleView()">
<div id="messageBox"></div>
</div>
<div id="rightPanel">
<div class="row"><i>Status</i></div>
@@ -1316,9 +1386,8 @@ async def viewer(request):
<div id="presetDialog">
<div id="presetLeftPanel">
</div>
<div id="presetCenterPanel">
<button id="toggleViewButton" onclick="toggleView()"></button>
<div id="messageBox" onclick="toggleView()"></div>
<div id="presetCenterPanel" onclick="toggleView()">
<div id="messageBox"></div>
</div>
<div id="presetRightPanel">
<div class="row">
@@ -1373,8 +1442,8 @@ async def get_status(request):
setstate.clear()
data = {}
data["status_info"] = status_info
data["stream_mtime"] = frame_mtime
data["preview_mtime"] = {key: state[key][0] for key in state if key.endswith("PreviewBox")}
data["log"] = {key: state[key] for key in state if key.endswith("LogBox")}
data["message"] = state["message"]
data["update_and_reload"] = state["update_and_reload"]
state["update_and_reload"] = False
@@ -1439,8 +1508,8 @@ async def get_wd14tag(request):
stt = await request.json()
state.update(stt);
tags = []
if frame_buffer is not None:
tags = await wd14tagger.tag(Image.fromarray(iio.imread(frame_buffer, index=0)), "wd-v1-4-moat-tagger-v2.onnx", state["wd14th"], state["wd14cth"], exclude_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})
@@ -1448,15 +1517,12 @@ async def get_wd14tag(request):
async def set_frame(request):
if request.remote not in allowed_ips:
raise HTTPForbidden()
frame = base64.b64decode((await request.text()).split(',', 1)[1])
global frame_buffer
frame_buffer = [frame]
global frame_mtime
pngdata = base64.b64decode((await request.text()).split(',', 1)[1])
with BytesIO(pngdata) as data:
with BytesIO() as output:
iio.imwrite(output, [iio.imread(data)], format="png", extension=".apng", compression=STREAM_COMPRESSION)
frame_buffer = output.getvalue()
frame_mtime = time.time()
frame_mtime = time.time()
return web.Response()
@@ -1514,6 +1580,8 @@ async def load_preset(request):
buf = {"lora": buf.get("lora", "")}
state.update(stt)
param.clear()
param.update(default_param)
param.update(buf)
state["presetTitle"] = Path(state["presetFile"]).stem
time.sleep(UPDATE_DELAY)
@@ -1606,7 +1674,7 @@ class FlipStreamTextBox:
def run(self, label, default, **kwargs):
global frame_updating
frame_updating = time.time()
param.setdefault(label, btoa_utf8(default))
param.setdefault(label, btoa_utf8(default))
return (atob_utf8(param[label]),)
@@ -1707,7 +1775,7 @@ class FlipStreamFileSelect:
return [folder_path]
return folder_paths.get_filename_list(folder_name)
else:
return list(map(str, Path(folder_path).glob("*.*")))
return [str(p.relative_to(folder_path)) for p in Path(folder_path).glob("*.*")]
@classmethod
def INPUT_TYPES(s):
@@ -1801,12 +1869,33 @@ class FlipStreamPreviewBox:
buf = np.array(tensor[0].cpu().numpy() * 255, dtype=np.uint8)
image = Image.fromarray(buf)
image.thumbnail((256, 256))
with BytesIO() as output:
iio.imwrite(output, np.array(image), format="png", extension=".png", compression=STREAM_COMPRESSION)
with io.BytesIO() as output:
image.save(output, format="PNG", compress_level=STREAM_COMPRESSION)
state[label + "PreviewBox"] = (time.time(), output.getvalue())
return ()
class FlipStreamLogBox:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"label": ("STRING", {"default": "empty"}),
"log": ("STRING",),
"rows": ("INT", {"default": 3}),
}
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "FlipStreamViewer"
def run(self, label, log, **kwargs):
state[label + "LogBox"] = btoa_utf8(log)
return ()
class FlipStreamSetUpdateAndReload:
@classmethod
def INPUT_TYPES(s):
@@ -1993,7 +2082,12 @@ class FlipStreamTextReplace:
CATEGORY = "FlipStreamViewer"
def run(self, text, find, replace, value=None):
return (text.replace(find, replace.format(value)),)
for word in find.split(","):
word = word.strip()
if not word:
continue
text = text.replace(word, replace.format(value))
return (text,)
class FlipStreamScreenGrabber:
@@ -2085,15 +2179,15 @@ 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)
try:
with iio.imopen(path, "r") as file:
buf = [file.read(index=i) for i in range(first, first+(frames-1)*step+1, step)]
buf = np.stack(buf).astype(np.float32) / 255
enable = buf.shape[0] == frames
image = torch.from_numpy(buf) if enable else None
return (image, enable)
except:
return (torch.zeros([1, 32, 32, 3]), False)
#try:
with iio.imopen(path, "r") as file:
buf = [file.read(index=i) for i in range(first, first+(frames-1)*step+1, step)]
buf = np.stack(buf).astype(np.float32) / 255
enable = buf.shape[0] == frames
image = torch.from_numpy(buf) if enable else None
return (image, enable)
#except:
# return (torch.zeros([1, 32, 32, 3]), False)
class FlipStreamSource:
@@ -2377,8 +2471,9 @@ class FlipStreamChat:
"seed": ("INT", {"default": -1}),
"max_tokens": ("INT", {"default": 128, "min": 0}),
"presence_penalty": ("FLOAT", {"default": 0, "min": 0}),
"frequency_penalty": ("FLOAT", {"default": 0, "min": 0}),
"repeat_penalty": ("FLOAT", {"default": 1.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})
},
"optional": {
"chat_model": ("CHAT_MODEL",),
@@ -2412,7 +2507,7 @@ class FlipStreamChat:
self.model.close()
self.model._FlipStreamChat_is_closed = True
def chat(self, system, user, stop, messages, **kwargs):
def chat(self, system, user, stop, messages, response_format, **kwargs):
if system != self.system:
self.system = system
messages.clear()
@@ -2422,7 +2517,11 @@ class FlipStreamChat:
messages[0] = dict(role="system", content=system)
if user:
messages.append(dict(role="user", content=user))
return self.model.create_chat_completion(messages, stop=list(filter(str.strip, stop.split(","))), **kwargs)["choices"][0]["message"]
if response_format:
response_format = json.loads(response_format)
else:
response_format = None
return self.model.create_chat_completion(messages, stop=list(filter(str.strip, stop.split(","))), response_format=response_format, **kwargs)["choices"][0]["message"]
def run(self, model_file, n_ctx, n_gpu_layers, unload_other_models, close_after_use, system, user, instant, max_history, stop, chat_model=None, messages=None, **kwargs):
if unload_other_models:
@@ -2441,7 +2540,7 @@ class FlipStreamChat:
if max_history == 0:
messages.clear()
elif max_history >= 1:
messages[:] = messages[-max_history:]
messages[:] = messages[:max_history]
self.load_model(model_file, n_ctx, n_gpu_layers)
if instant:
res = self.chat(system, user, stop, messages.copy(), **kwargs)
@@ -2457,6 +2556,33 @@ class FlipStreamChat:
return (self.model, output, messages)
class FlipStreamParseJson:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"json_input": ("STRING", {"default": "", "multiline": True}),
"keys": ("STRING", {"default": "", "multiline": True}),
"joinstr": ("STRING", {"default": ",", "multiline": True}),
"ignore_error": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "FlipStreamViewer"
def run(self, json_input, keys, joinstr, ignore_error):
value = []
try:
for key in keys.split(","):
value.append(json.loads(json_input, strict=False)[key.strip()])
except Exception as e:
if not ignore_error:
raise RuntimeError(f"FlipStreamParseJsonItem: Invalid JSON input: {e}: {json_input}")
return (joinstr.join(value),)
class FlipStreamBatchPrompt:
@classmethod
def INPUT_TYPES(s):
@@ -2587,18 +2713,26 @@ class FlipStreamViewer:
CATEGORY = "FlipStreamViewer"
def run(self, tensor, fps, **kwargs):
global frame_updating
global frame_buffer
global frame_mtime
fb = []
buf = (tensor.detach().cpu().numpy() * 255).astype(np.uint8)
buf = np.concatenate([buf, np.flip(buf, axis=0)])
with BytesIO() as output:
iio.imwrite(output, buf, format='png', extension=".apng", compression=STREAM_COMPRESSION, fps=fps)
frame_buffer = output.getvalue()
frame_mtime = time.time()
if tensor.shape[0] != 1:
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_updating
global frame_mtime
global frame_fps
frame_buffer = fb
frame_updating = None
frame_mtime = time.time()
frame_fps = fps
return ()
NODE_CLASS_MAPPINGS = {
"FlipStreamSection": FlipStreamSection,
"FlipStreamSlider": FlipStreamSlider,
@@ -2614,6 +2748,7 @@ NODE_CLASS_MAPPINGS = {
"FlipStreamFileSelect_Input": FlipStreamFileSelect_Input,
"FlipStreamFileSelect_Output": FlipStreamFileSelect_Output,
"FlipStreamPreviewBox": FlipStreamPreviewBox,
"FlipStreamLogBox": FlipStreamLogBox,
"FlipStreamSetUpdateAndReload": FlipStreamSetUpdateAndReload,
"FlipStreamSetMessage": FlipStreamSetMessage,
"FlipStreamSetParam": FlipStreamSetParam,
@@ -2631,6 +2766,7 @@ NODE_CLASS_MAPPINGS = {
"FlipStreamRembg": FlipStreamRembg,
"FlipStreamSegMask": FlipStreamSegMask,
"FlipStreamChat": FlipStreamChat,
"FlipStreamParseJson": FlipStreamParseJson,
"FlipStreamBatchPrompt": FlipStreamBatchPrompt,
"FlipStreamFilmVfi": FlipStreamFilmVfi,
"FlipStreamViewer": FlipStreamViewer,
@@ -2651,6 +2787,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FlipStreamFileSelect_Input": "FlipStreamFileSelect_Input",
"FlipStreamFileSelect_Output": "FlipStreamFileSelect_Output",
"FlipStreamPreviewBox": "FlipStreamPreviewBox",
"FlipStreamLogBox": "FlipStreamLogBox",
"FlipStreamSetUpdateAndReload": "FlipStreamSetUpdateAndReload",
"FlipStreamSetMessage": "FlipStreamSetMessage",
"FlipStreamSetParam": "FlipStreamSetParam",
@@ -2668,6 +2805,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FlipStreamRembg": "FlipStreamRembg",
"FlipStreamSegMask": "FlipStreamSegMask",
"FlipStreamChat": "FlipStreamChat",
"FlipStreamParseJson": "FlipStreamParseJson",
"FlipStreamBatchPrompt": "FlipStreamBatchPrompt",
"FlipStreamFilmVfi": "FlipStreamFilmVfi",
"FlipStreamViewer": "FlipStreamViewer",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_flipstreamviewer"
description = "ComfyUI_FlipStreamViewer is a tool that provides a customizable viewer interface for flipping images with frame interpolation."
version = "3.3.2"
version = "3.5.0"
license = {file = "LICENSE.txt"}
[project.urls]
-1
View File
@@ -1,2 +1 @@
imageio
mss