Update Stream Response VisionLMs API and many..

This commit is contained in:
AhBumm
2025-11-11 21:13:05 +08:00
parent 55ab4f0cf1
commit 28f2ee5abe
4 changed files with 576 additions and 198 deletions
+36 -5
View File
@@ -1,16 +1,20 @@
from .billbum_modified import *
from .nodes4tuzi import (
BillBum_Modified_StreamResponse_LLM_API,
Url2Image,
RegTuziChatResponse,
LoadVideoFromUrlVHS,
LoadVideoFromUrlComfyIO,
)
# Exporting the node classes for ComfyUI to discover
NODE_CLASS_MAPPINGS = {
"BillBum_Modified_Dalle_API_Node": BillBum_Modified_Dalle_API_Node,
"BillBum_Modified_LLM_API_Node": BillBum_Modified_LLM_API_Node,
"BillBum_Modified_img2b64_url_Node": BillBum_Modified_img2url_Node,
"BillBum_Modified_img2b64_url_Node": BillBum_Modified_img2b64url_Node,
"BillBum_Modified_VisionLM_API_Node": BillBum_Modified_VisionLM_API_Node,
"BillBum_Modified_SD3_API_Node": BillBum_Modified_SD3_API_Node,
"BillBum_Modified_Base64_Url2Img_Node": BillBum_Modified_Base64_Url2Img_Node,
"BillBum_Modified_ImageSplit_Node": BillBum_Modified_ImageSplit_Node,
"BillBum_Modified_Base64_Url2Data_Node": BillBum_Modified_Base64_Url2Data_Node,
"BillBum_Modified_Structured_LLM_Node(Imperfect)": BillBum_Modified_Structured_LLM_Node,
"BillBum_Modified_Flux_API_Node": BillBum_Modified_Flux_API_Node,
"BillBum_Modified_RegText_Node": BillBum_Modified_RegText_Node,
"BillBum_Modified_DropoutToken_Node": BillBum_Modified_DropoutToken_Node,
"BillBum_Modified_Image_API_Call_Node": BillBum_Modified_Image_API_Call_Node,
@@ -22,7 +26,34 @@ NODE_CLASS_MAPPINGS = {
"BillBum_Modified_LLM_ForceStream_Mode": BillBum_Modified_LLM_ForceStream_Mode,
"BillBum_Modified_GPTImage1_API_Node": BillBum_Modified_GPTImage1_API_Node,
"BillBum_Modified_Flux_API_with_imgInput": BillBum_Modified_Flux_API_Node_imgInput,
"billbum_modified_stream_response_llm_api": BillBum_Modified_StreamResponse_LLM_API,
"url2image": Url2Image,
"reg_tuzi_chat_response": RegTuziChatResponse,
"load_video_from_url": LoadVideoFromUrlVHS,
"load_video_from_url_comfy_core": LoadVideoFromUrlComfyIO,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"billbum_modified_stream_response_llm_api": "API Node for Stream Response LLMs",
"url2image": "Load Image from URL (BillBum)",
"reg_tuzi_chat_response": "Tuzi Chat Response Parser",
"load_video_from_url": "Load Video From URL (VHS Compatible)",
"load_video_from_url_comfy_core": "Load&Save Video From URL (Comfy Core)",
"BillBum_Modified_Dalle_API_Node": "Dall-E Custom API Node",
"BillBum_Modified_LLM_API_Node": "Custom LLM API Node (Old)",
"BillBum_Modified_img2b64_url_Node": "Image to Base64 URL Node",
"BillBum_Modified_VisionLM_API_Node": "Vision LLMs API Node (Old)",
"BillBum_Modified_SD3_API_Node": "Stable Diffusion 3 API Node",
"BillBum_Modified_Base64_Url2Img_Node": "Base64 URL to Image Node",
"BillBum_Modified_RegText_Node": "Regular ResponseText to 1linePrompt Node",
"BillBum_Modified_DropoutToken_Node": "Dropout by MaxToken Node",
"BillBum_Modified_Image_API_Call_Node": "Custom Image Generation API Call Node",
"BillBum_Modified_Recraft_API_Node": "Custom Recraft API Node",
"Text_Concat": "Concat Text Strings Node",
"Input_Text": "Input Text",
"BillBum_Modified_Ideogram_API_Node": "Custom Ideogram API Node",
"BillBum_NonSysPrompt_VLM_API_Node": "Non-System Prompt VLMs API Node",
"BillBum_Modified_LLM_ForceStream_Mode": "LLM StreamResponse Node (Old)",
"BillBum_Modified_GPTImage1_API_Node": "Custom GPTImage1 API Node",
"BillBum_Modified_Flux_API_with_imgInput": "Custom Flux API Node",
}
+3 -191
View File
@@ -149,7 +149,7 @@ class Text_Concat:
CATEGORY = "string processing"
def text_concat(self, text_1, text_2):
text = f"{text_1} {text_2}"
text = f"{text_1}{text_2}"
return (text,)
class Input_Text:
@@ -264,7 +264,7 @@ class BillBum_Modified_LLM_API_Node:
}),
"api_url": ("STRING", {
"multiline": False,
"default": "https://api.hyprlab.io/v2",
"default": "https://api.hyprlab.io/v1",
}),
"api_key": ("STRING", {
"multiline": False,
@@ -394,62 +394,6 @@ class BillBum_Modified_LLM_ForceStream_Mode:
return (full_content,seed,)
class BillBum_Modified_Structured_LLM_Node:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"defaultInput": True},),
"model": ("STRING", {
"default": "gpt-4o-mini",
}),
"api_url": ("STRING", {
"multiline": False,
"default": "https://api.hyprlab.io/v1",
}),
"api_key": ("STRING", {
"multiline": False,
"default": "YOUR_API_KEY_HERE",
}),
"system_prompt": ("STRING", {"defaultInput": True},),
"output_format": ("STRING", {"defaultInput": True},),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("structured_str",)
FUNCTION = "get_llm_structured_response"
CATEGORY = "BillBum_API"
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1.25, min=5, max=30))
def get_llm_structured_response(self, prompt, model, api_url, api_key, system_prompt, output_format, seed):
META_SCHEMA = json.loads(output_format)
random.seed(seed)
client = OpenAI(
api_key=api_key,
base_url=api_url
)
completion = client.chat.completions.create(
model=model,
response_format={"type": "json_schema", "json_schema": META_SCHEMA},
messages=[
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': "Description:\n" + prompt}
]
)
response_dict = json.loads(completion.choices[0].message.content)
data = response_dict.get("data", [])
return (data,)
class BillBum_Modified_VisionLM_API_Node:
def __init__(self):
@@ -762,7 +706,7 @@ Are you in agreement with these instructions? Please respond with "Ok!"
)
return (completion.choices[0].message.content, seed, model, api_url, api_key)
class BillBum_Modified_img2url_Node:
class BillBum_Modified_img2b64url_Node:
"""
A ComfyUI node to convert an image file to a base64 encoded string.
"""
@@ -890,62 +834,6 @@ class BillBum_Modified_SD3_API_Node:
raise Exception(f"Image URL not found in response: {response_json}")
return (image_url,)
class BillBum_Modified_Flux_API_Node:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("STRING", {"default": "flux-1.1-pro"}),
"prompt": ("STRING", {"defaultInput": True}),
"width": ("INT", {"default": 1024, "min": 256, "max": 1440, "step": 32, "display": "number"}),
"height": ("INT", {"default": 1024, "min": 256, "max": 1440, "step": 32, "display": "number"}),
"steps": ("INT", {"default": 20, "min": 1, "max": 50, "step": 1, "display": "number"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 10000}),
"api_url": ("STRING", {"multiline": False, "default": "https://api.hyprlab.io/v1/images/generations"}),
"api_key": ("STRING", {"default": "YOUR_API_KEY_HERE"}),
},
}
RETURN_TYPES = ("STRING", "INT",)
RETURN_NAMES = ("base64_url", "seed",)
FUNCTION = "get_t2i_image"
CATEGORY = "BillBum_API"
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1.25, min=5, max=30))
def get_t2i_image(self, model, prompt, width, height, steps, api_url, seed, api_key):
random.seed(seed)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
data = {
"model": model,
"prompt": prompt,
"steps": steps,
"height": height,
"width": width,
"response_format": "b64_json",
"output_format": "webp"
}
response = requests.post(api_url, headers=headers, json=data)
print(f"HTTP status code: {response.status_code}")
response.raise_for_status()
response_json = response.json()
try:
b64_string = response_json['data'][0]['b64_json']
base64_url = f"data:image/webp;base64,{b64_string}"
return (base64_url, seed)
except (KeyError, IndexError) as e:
raise Exception(f"Unexpected response format: {e}")
class BillBum_Modified_Flux_API_Node_imgInput:
def __init__(self):
@@ -1224,82 +1112,6 @@ class BillBum_Modified_Base64_Url2Img_Node:
image = Image.open(io.BytesIO(image_data))
return (pil2tensor(image),)
class BillBum_Modified_ImageSplit_Node:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "split_image"
CATEGORY = "BillBum Image Processing"
def split_image(self, image):
# Convert tensor to PIL image if necessary
img_pil = Image.fromarray((image.squeeze(0).numpy() * 255).astype('uint8')) if isinstance(image, torch.Tensor) else image
# Handle different image sizes
if img_pil.size == (1024, 1024):
# If the image is 1024x1024, return it as is
return (image,)
elif img_pil.size == (2048, 1024):
# If the image is 2048x1024, split it into two 1024x1024 images
box_coordinates = [(0, 0, 1024, 1024), (1024, 0, 2048, 1024)]
elif img_pil.size == (2048, 2048):
# If the image is 2048x2048, split it into four 1024x1024 images
box_coordinates = [(0, 0, 1024, 1024), (1024, 0, 2048, 1024), (0, 1024, 1024, 2048), (1024, 1024, 2048, 2048)]
else:
raise ValueError("Input image must be either 1024x1024, 2048x1024, or 2048x2048.")
# Define tolerance for detecting near-black images
tolerance = 0.08 # 8% tolerance
# Crop and convert the sub-images to tensors, while filtering out near-black images
sub_images = []
for box in box_coordinates:
cropped_img = img_pil.crop(box)
np_img = np.array(cropped_img).astype(np.float32) / 255.0
# Check if the image is not near-black
if not np.all(np_img <= tolerance):
sub_images.append(torch.from_numpy(np_img))
# If all sub-images are near-black, return an empty tensor
if not sub_images:
raise ValueError("All cropped sub-images are near-black.")
# Stack valid sub-images to create a batch tensor
batch_tensor = torch.stack(sub_images)
return (batch_tensor,)
class BillBum_Modified_Base64_Url2Data_Node:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base64_url": ("STRING", {"defaultInput": True},),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "convert"
CATEGORY = "BillBum String Processing"
def convert(self, base64_url_string):
try:
base64_data_string = base64_url_string.split(",", 1)[1]
except IndexError:
raise ValueError("Invalid base64 URL string format.")
return (base64_data_string,)
class BillBum_Modified_RegText_Node:
def __init__(self):
+535
View File
@@ -0,0 +1,535 @@
import tenacity
import random
from openai import OpenAI
import io
import re
from PIL import Image
import numpy as np
import torch
import requests
import math
import base64
from comfy.utils import common_upscale
import subprocess
import tempfile
import os
from urllib.parse import urlparse
import folder_paths
import shutil
from comfy_api.latest import ui
from comfy_api.latest import io as comfyio
from comfy_api.input_impl import VideoFromFile
## ======== Utils Functions ========
def downscale_input(image):
samples = image.movedim(-1,1)
#downscaling input images to roughly the same size as the outputs
total = int(1536 * 1024)
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
if scale_by >= 1:
return image
width = round(samples.shape[3] * scale_by)
height = round(samples.shape[2] * scale_by)
s = common_upscale(samples, width, height, "lanczos", "disabled")
s = s.movedim(1,-1)
return s
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
## ======== Nodes Classes ========
class BillBum_Modified_StreamResponse_LLM_API:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"forceInput": True}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"model": ("STRING", {"default": "gpt-4o-mini"}),
"api_url": ("STRING", {"multiline": False, "default": "https://api.tu-zi.com/v1"}),
"api_key": ("STRING", {"multiline": False, "default": "YOUR_API_KEY_HERE"}),
"temperature": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.05}),
"enable_thinking": ("COMBO", {
"options": ["true", "false", "none"],
"default": "none",
"tooltip": "only true/false would append 'enable_thinking' to request body",
}),
},
"optional": {
"system_prompt": ("STRING", {"forceInput": True, "default": None}),
"images": ("IMAGE", {"default": None, "tooltip": "Use Any Image Batch Nodes to input multiple images"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("LLM RESPONSE",)
FUNCTION = "get_llm_stream_response"
CATEGORY = "BillBum_API/Stream Response"
@staticmethod
def _encode_images_to_base64(images):
if images is None:
return []
if images.dim() == 3:
images = images.unsqueeze(0)
images = downscale_input(images)
encoded_images = []
for idx in range(images.shape[0]):
tensor_image = images[idx].clamp(0.0, 1.0)
pil_image = tensor2pil(tensor_image)
buffer = io.BytesIO()
pil_image.save(buffer, format="PNG")
encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
encoded_images.append(encoded)
return encoded_images
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1.25, min=5, max=30), stop=tenacity.stop_after_attempt(3))
def get_llm_stream_response(
self,
prompt,
seed,
model,
api_url,
api_key,
temperature,
enable_thinking,
images=None,
system_prompt=None,
):
random.seed(seed)
client = OpenAI(api_key=api_key, base_url=api_url)
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
user_content = []
if prompt:
user_content.append({"type": "text", "text": prompt})
for encoded_image in self._encode_images_to_base64(images):
user_content.append({
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{encoded_image}"},
})
if not user_content:
raise ValueError("Prompt and images cannot both be empty.")
messages.append({"role": "user", "content": user_content})
request_kwargs = {
"model": model,
"messages": messages,
"stream": True,
}
if temperature != 0.0:
request_kwargs["temperature"] = temperature
extra_body = {}
if enable_thinking == "true":
extra_body["enable_thinking"] = True
elif enable_thinking == "false":
extra_body["enable_thinking"] = False
if extra_body:
request_kwargs["extra_body"] = extra_body
completion = client.chat.completions.create(**request_kwargs)
full_content = ""
for chunk in completion:
if chunk.choices and chunk.choices[0].delta.content is not None:
delta = chunk.choices[0].delta.content
full_content += delta
print(delta, end="")
return (full_content,)
class Url2Image:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"url": ("STRING", {"multiline": False, "default": ""}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "get_url_image"
CATEGORY = "BillBum_API/Utils"
def _load_image_bytes(self, entry: str) -> bytes:
if entry.startswith("data:"):
_, base64_data = entry.split(",", 1)
return base64.b64decode(base64_data)
if entry.startswith(("http://", "https://")):
response = requests.get(entry, timeout=10)
response.raise_for_status()
return response.content
return base64.b64decode(entry)
def _decode_entry(self, entry: str):
image_data = self._load_image_bytes(entry)
image = Image.open(io.BytesIO(image_data))
if image.mode != "RGBA":
image = image.convert("RGBA")
return np.array(image, dtype=np.float32) / 255.0
def get_url_image(self, url):
if not url:
return (None,)
entries = []
for raw_line in url.replace("\r", "").split("\n"):
line = raw_line.strip()
if not line:
continue
if line.startswith("data:"):
entries.append(line)
else:
for part in line.split(","):
part = part.strip()
if part:
entries.append(part)
if not entries:
return (None,)
decoded_images = []
for entry in entries:
try:
decoded_images.append(self._decode_entry(entry))
except Exception as e:
print(f"Url2Image: Can't decode {entry}: {e}")
if not decoded_images:
return (None,)
max_h = max(img.shape[0] for img in decoded_images)
max_w = max(img.shape[1] for img in decoded_images)
batches = []
for img in decoded_images:
h, w, _ = img.shape
padded = np.zeros((max_h, max_w, 4), dtype=np.float32)
padded[:h, :w, :] = img
batches.append(padded)
image_tensor = torch.from_numpy(np.stack(batches, axis=0))
return (image_tensor,)
class LoadVideoFromUrlComfyIO(comfyio.ComfyNode):
def __init__(self):
pass
@classmethod
def define_schema(cls):
return comfyio.Schema(
node_id="load_video_from_url_comfy_core",
display_name="Load&Save Video From URL (Comfy Core)",
category="BillBum_API/Utils",
inputs=[
comfyio.String.Input("url", default="", tooltip="http/https url"),
comfyio.String.Input("filename_prefix", default="video_files/url_download"),
],
outputs=[comfyio.Video.Output("video")],
hidden=[comfyio.Hidden.prompt, comfyio.Hidden.extra_pnginfo],
is_output_node=True,
)
@staticmethod
def _extension_from_url(url: str) -> str:
ext = os.path.splitext(urlparse(url).path)[1].lower()
if ext in {".mp4", ".mov", ".mkv", ".webm", ".gif"}:
return ext
return ".mp4"
@staticmethod
def _download_to_temp(url: str, suffix: str) -> str:
with requests.get(url, stream=True, timeout=30) as resp:
resp.raise_for_status()
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
tmp.write(chunk)
return tmp.name
@classmethod
def execute(cls, url, filename_prefix) -> comfyio.NodeOutput:
url = (url or "").strip()
if not url:
raise ValueError("URL cannot be empty.")
suffix = cls._extension_from_url(url)
temp_path = cls._download_to_temp(url, suffix)
try:
video_temp = VideoFromFile(temp_path)
width, height = video_temp.get_dimensions()
full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
filename_prefix or "temp",
folder_paths.get_output_directory(),
width,
height,
)
output_name = f"{filename}_{counter:05}{suffix}"
final_path = os.path.join(full_output_folder, output_name)
shutil.move(temp_path, final_path)
video = VideoFromFile(final_path)
preview = ui.PreviewVideo([ui.SavedResult(output_name, subfolder, comfyio.FolderType.output)])
return comfyio.NodeOutput(video, ui=preview)
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
class LoadVideoFromUrlVHS:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"url": ("STRING", {"multiline": False, "default": ""}),
}
}
RETURN_TYPES = ("IMAGE", "VHS_VIDEOINFO",)
RETURN_NAMES = ("image", "video_info",)
FUNCTION = "load_video"
CATEGORY = "BillBum_API/Utils"
def _download_video(self, url: str) -> str:
if not url.startswith(("http://", "https://")):
raise ValueError("仅支持 http/https URL。")
try:
with requests.get(url, timeout=15, stream=True) as response:
response.raise_for_status()
suffix = os.path.splitext(url)[1] or ".mp4"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
tmp_file.write(chunk)
return tmp_file.name
except requests.exceptions.RequestException as e:
raise ConnectionError(f"无法下载视频: {e}")
def _get_video_metadata(self, filepath: str):
ffmpeg_path = "ffmpeg"
width = height = 0
fps = 30.0
duration = 0.0
try:
proc = subprocess.run(
[ffmpeg_path, "-i", filepath, "-f", "null", "-"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
check=False,
)
stderr_output = proc.stderr.decode("utf-8", errors="ignore")
for line in stderr_output.splitlines():
if "Stream" in line and "Video" in line:
size_match = re.search(r"(\d{2,})x(\d+)", line)
if size_match:
width, height = map(int, size_match.group(0).split("x"))
tbr_match = re.search(r"([\d\.]+) tbr", line)
if tbr_match:
fps = float(tbr_match.group(1))
else:
fps_match = re.search(r"([\d\.]+) fps", line)
if fps_match:
fps = float(fps_match.group(1))
break
duration_match = re.search(r"Duration: (\d{2}):(\d{2}):(\d{2})\.(\d+)", stderr_output)
if duration_match:
h, m, s, ms_part = duration_match.groups()
duration = (
int(h) * 3600
+ int(m) * 60
+ int(s)
+ float(f"0.{ms_part}")
)
except FileNotFoundError as e:
raise RuntimeError("未检测到 ffmpeg,可在系统 PATH 中安装。") from e
return width, height, fps, duration
def _extract_frames(self, filepath: str, width: int, height: int):
if width <= 0 or height <= 0:
raise RuntimeError("无法确定视频分辨率。")
command = [
"ffmpeg",
"-i",
filepath,
"-f",
"rawvideo",
"-pix_fmt",
"rgba",
"pipe:1",
]
frame_size = width * height * 4
frames = []
with subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=10 ** 7,
) as proc:
try:
while True:
frame_bytes = proc.stdout.read(frame_size)
if not frame_bytes or len(frame_bytes) < frame_size:
break
frame_np = np.frombuffer(frame_bytes, dtype=np.uint8).reshape((height, width, 4))
frames.append(frame_np.astype(np.float32) / 255.0)
finally:
proc.stdout.close()
proc.wait()
if not frames:
raise RuntimeError("未能从视频提取任何帧。")
return frames
def load_video(self, url: str):
if not url:
raise ValueError("URL 输入为空。")
temp_path = self._download_video(url)
try:
width, height, fps, source_duration = self._get_video_metadata(temp_path)
if fps <= 0:
fps = 30.0
frames_np = self._extract_frames(temp_path, width, height)
image_tensor = torch.from_numpy(np.stack(frames_np))
loaded_frames = image_tensor.shape[0]
loaded_duration = loaded_frames / fps if fps > 0 else 0.0
source_duration = source_duration or loaded_duration
source_frame_count = int(round(source_duration * fps)) if source_duration and fps > 0 else loaded_frames
video_info = {
"source_fps": fps,
"source_frame_count": source_frame_count,
"source_duration": source_duration,
"source_width": width,
"source_height": height,
"loaded_fps": fps,
"loaded_frame_count": loaded_frames,
"loaded_duration": loaded_duration,
"loaded_width": width,
"loaded_height": height,
}
return (image_tensor, video_info)
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
class RegTuziChatResponse:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"response": ("STRING", {"forceInput": True}),
"content_type": ("COMBO", {
"options": ["text", "image", "video"],
"default": "text",
})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("CONTENT",)
FUNCTION = "reg_chat_response"
CATEGORY = "BillBum_API/Utils"
def reg_chat_response(self, response, content_type):
if content_type == "text":
out_str = response
elif content_type == "image":
image_urls = []
markdown_urls = re.findall(
r'!\[[^\]]*\]\((https?://[^\s\)]+)\)',
response,
flags=re.IGNORECASE,
)
image_urls.extend(markdown_urls)
file_urls = re.findall(
r'(https?://[^\s\)\]]+\.(?:jpg|jpeg|png|webp|gif|bmp|tif|tiff))',
response,
flags=re.IGNORECASE,
)
for url in file_urls:
if url not in image_urls:
image_urls.append(url)
unique_urls = []
seen_names = set()
for url in image_urls:
filename = os.path.basename(urlparse(url).path)
if not filename:
filename = url
if filename.lower() in seen_names:
continue
seen_names.add(filename.lower())
unique_urls.append(url)
out_str = ",".join(unique_urls)
elif content_type == "video":
marker = "[⏬ 下载视频]("
out_str = ""
start = response.find(marker)
if start != -1:
start += len(marker)
end = response.find(")", start)
if end != -1:
out_str = response[start:end]
# === Fallback to find .mp4 URLs in response ===
if not out_str:
mp4_urls = re.findall(r"(https?://[^\s\)\]]+\.mp4)", response, flags=re.IGNORECASE)
if mp4_urls:
out_str = mp4_urls[-1]
return (out_str,)
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_billbum_api_nodes"
description = "API call node for Third-party platforms both official and local. Support VLMs LLMs Dalle3 Flux-Pro(Support kontext now!!) etc(new Support gpt-image-1!!). And some little tools: img to b64 url, b64 url to img, b64 url to b64 data, reg text to word and ',' only, etc."
version = "1.1.5"
description = "API call node for Third-party platforms both official and local. Support VLMs LLMs Dalle3 Flux-Pro(Support kontext, banana etc... now!! and new Support gpt-image-1!!). And some little tools: img to b64 url, b64 url to img, b64 url to b64 data, reg text to word and ',' only, etc."
version = "1.1.6"
license = {file = "LICENSE"}
dependencies = ["tenacity", "openai", "pillow", "requests", "torch", "numpy", "tiktoken"]