Files
siliconflow-BizyAir/llm.py
2025-02-21 17:48:43 +08:00

525 lines
17 KiB
Python

import asyncio
import json
import os
import aiohttp
import comfy
import folder_paths
import numpy as np
from aiohttp import web
from PIL import Image
from server import PromptServer
from bizyair.common.env_var import BIZYAIR_SERVER_ADDRESS
from bizyair.image_utils import decode_data, encode_comfy_image, encode_data
from .utils import (
decode_and_deserialize,
get_api_key,
get_llm_response,
get_vlm_response,
send_post_request,
serialize_and_encode,
)
async def fetch_all_models(api_key):
url = "https://api.siliconflow.cn/v1/models"
headers = {"accept": "application/json", "authorization": f"Bearer {api_key}"}
params = {"type": "text", "sub_type": "chat"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(
url, headers=headers, params=params, timeout=10
) as response:
if response.status == 200:
data = await response.json()
all_models = [model["id"] for model in data["data"]]
return all_models
else:
print(f"Error fetching models: HTTP Status {response.status}")
return []
except aiohttp.ClientError as e:
print(f"Error fetching models: {e}")
return []
except asyncio.exceptions.TimeoutError:
print("Request to fetch models timed out")
return []
@PromptServer.instance.routes.post("/bizyair/get_silicon_cloud_llm_models")
async def get_silicon_cloud_llm_models_endpoint(request):
data = await request.json()
api_key = data.get("api_key", get_api_key())
all_models = await fetch_all_models(api_key)
llm_models = [model for model in all_models if "vl" not in model.lower()]
llm_models.append("No LLM Enhancement")
return web.json_response(llm_models)
@PromptServer.instance.routes.post("/bizyair/get_silicon_cloud_vlm_models")
async def get_silicon_cloud_vlm_models_endpoint(request):
data = await request.json()
api_key = data.get("api_key", get_api_key())
all_models = await fetch_all_models(api_key)
vlm_models = [model for model in all_models if "vl" in model.lower()]
vlm_models.append("No VLM Enhancement")
return web.json_response(vlm_models)
class SiliconCloudLLMAPI:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
default_system_prompt = """你是一个 stable diffusion prompt 专家,为我生成适用于 Stable Diffusion 模型的prompt。 我给你相关的单词,你帮我扩写为适合 Stable Diffusion 文生图的 prompt。要求: 1. 英文输出 2. 除了 prompt 外,不要输出任何其它的信息 """
return {
"required": {
"model": ((), {}),
"system_prompt": (
"STRING",
{
"default": default_system_prompt,
"multiline": True,
"dynamicPrompts": True,
},
),
"user_prompt": (
"STRING",
{
"default": "小猫,梵高风格",
"multiline": True,
"dynamicPrompts": True,
},
),
"max_tokens": ("INT", {"default": 512, "min": 100, "max": 1e5}),
"temperature": (
"FLOAT",
{"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.01},
),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "get_llm_model_response"
OUTPUT_NODE = False
CATEGORY = "☁️BizyAir/AI Assistants"
def get_llm_model_response(
self, model, system_prompt, user_prompt, max_tokens, temperature
):
if model == "No LLM Enhancement":
return {"ui": {"text": (user_prompt,)}, "result": (user_prompt,)}
response = get_llm_response(
model,
system_prompt,
user_prompt,
max_tokens,
temperature,
)
ret = json.loads(response)
text = ret["choices"][0]["message"]["content"]
return (text,) # if update ui: {"ui": {"text": (text,)}, "result": (text,)}
class SiliconCloudVLMAPI:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ((), {}),
"system_prompt": (
"STRING",
{
"default": "你是一个能分析图像的AI助手。请仔细观察图像,并根据用户的问题提供详细、准确的描述。",
"multiline": True,
},
),
"user_prompt": (
"STRING",
{
"default": "请描述这张图片的内容,并指出任何有趣或不寻常的细节。",
"multiline": True,
},
),
"images": ("IMAGE",),
"max_tokens": ("INT", {"default": 512, "min": 100, "max": 1e5}),
"temperature": (
"FLOAT",
{"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.01},
),
"detail": (["auto", "low", "high"], {"default": "auto"}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "get_vlm_model_response"
OUTPUT_NODE = False
CATEGORY = "☁️BizyAir/AI Assistants"
def get_vlm_model_response(
self, model, system_prompt, user_prompt, images, max_tokens, temperature, detail
):
if model == "No VLM Enhancement":
return (user_prompt,)
# 使用 encode_comfy_image 函数编码图像批次
encoded_images_json = encode_comfy_image(
images, image_format="WEBP", lossless=True
)
encoded_images_dict = json.loads(encoded_images_json)
# 提取所有编码后的图像
base64_images = list(encoded_images_dict.values())
response = get_vlm_response(
model,
system_prompt,
user_prompt,
base64_images,
max_tokens,
temperature,
detail,
)
ret = json.loads(response)
text = ret["choices"][0]["message"]["content"]
return (text,)
class BizyAirJoyCaption:
# refer to: https://huggingface.co/spaces/fancyfeast/joy-caption-pre-alpha
API_URL = f"{BIZYAIR_SERVER_ADDRESS}/supernode/joycaption2"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"do_sample": (["enable", "disable"],),
"temperature": (
"FLOAT",
{
"default": 0.5,
"min": 0.0,
"max": 2.0,
"step": 0.01,
"round": 0.001,
"display": "number",
},
),
"max_tokens": (
"INT",
{
"default": 256,
"min": 16,
"max": 512,
"step": 16,
"display": "number",
},
),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "joycaption"
CATEGORY = "☁️BizyAir/AI Assistants"
def joycaption(self, image, do_sample, temperature, max_tokens):
API_KEY = get_api_key()
SIZE_LIMIT = 1536
# device = image.device
_, w, h, c = image.shape
assert (
w <= SIZE_LIMIT and h <= SIZE_LIMIT
), f"width and height must be less than {SIZE_LIMIT}x{SIZE_LIMIT}, but got {w} and {h}"
payload = {
"image": None,
"do_sample": do_sample == "enable",
"temperature": temperature,
"max_new_tokens": max_tokens,
"caption_type": "Descriptive",
"caption_length": "any",
"extra_options": [],
"name_input": "",
"custom_prompt": "A descriptive caption for this image:\n",
}
auth = f"Bearer {API_KEY}"
headers = {
"accept": "application/json",
"content-type": "application/json",
"authorization": auth,
}
input_image = encode_data(image, disable_image_marker=True)
payload["image"] = input_image
ret: str = send_post_request(self.API_URL, payload=payload, headers=headers)
ret = json.loads(ret)
try:
if "result" in ret:
ret = json.loads(ret["result"])
if ret["type"] == "error":
raise Exception(ret["message"])
except Exception as e:
raise Exception(f"Unexpected response: {ret} {e=}")
msg = ret["data"]
if msg["type"] not in (
"comfyair",
"bizyair",
):
raise Exception(f"Unexpected response type: {msg}")
caption = msg["data"]
return (caption,)
class BizyAirJoyCaption2:
def __init__(self):
pass
# refer to: https://huggingface.co/spaces/fancyfeast/joy-caption-pre-alpha
API_URL = f"{BIZYAIR_SERVER_ADDRESS}/supernode/joycaption2"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"do_sample": ([True, False],),
"temperature": (
"FLOAT",
{
"default": 0.5,
"min": 0.0,
"max": 2.0,
"step": 0.01,
"round": 0.001,
"display": "number",
},
),
"max_tokens": (
"INT",
{
"default": 256,
"min": 16,
"max": 512,
"step": 16,
"display": "number",
},
),
"caption_type": (
[
"Descriptive",
"Descriptive (Informal)",
"Training Prompt",
"MidJourney",
"Booru tag list",
"Booru-like tag list",
"Art Critic",
"Product Listing",
"Social Media Post",
],
),
"caption_length": (
["any", "very short", "short", "medium-length", "long", "very long"]
+ [str(i) for i in range(20, 261, 10)],
),
"extra_options": (
"STRING",
{
"default": "If there is a person/character in the image you must refer to them as {name}.",
"tooltip": "Extra options for the model",
"multiline": True,
},
),
"name_input": (
"STRING",
{
"default": "Jack",
"tooltip": "Name input is only used if an Extra Option is selected that requires it.",
},
),
"custom_prompt": (
"STRING",
{
"default": "",
"multiline": True,
},
),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "joycaption2"
CATEGORY = "☁️BizyAir/AI Assistants"
def joycaption2(
self,
image,
do_sample,
temperature,
max_tokens,
caption_type,
caption_length,
extra_options,
name_input,
custom_prompt,
):
API_KEY = get_api_key()
SIZE_LIMIT = 1536
_, w, h, c = image.shape
assert (
w <= SIZE_LIMIT and h <= SIZE_LIMIT
), f"width and height must be less than {SIZE_LIMIT}x{SIZE_LIMIT}, but got {w} and {h}"
payload = {
"image": None,
"do_sample": do_sample == True,
"temperature": temperature,
"max_new_tokens": max_tokens,
"caption_type": caption_type,
"caption_length": caption_length,
"extra_options": [extra_options],
"name_input": name_input,
"custom_prompt": custom_prompt,
}
auth = f"Bearer {API_KEY}"
headers = {
"accept": "application/json",
"content-type": "application/json",
"authorization": auth,
}
input_image = encode_data(image, disable_image_marker=True)
payload["image"] = input_image
ret: str = send_post_request(self.API_URL, payload=payload, headers=headers)
ret = json.loads(ret)
try:
if "result" in ret:
ret = json.loads(ret["result"])
if ret["type"] == "error":
raise Exception(ret["message"])
except Exception as e:
raise Exception(f"Unexpected response: {ret} {e=}")
msg = ret["data"]
if msg["type"] not in (
"comfyair",
"bizyair",
):
raise Exception(f"Unexpected response type: {msg}")
caption = msg["data"]
return (caption,)
class BizyAirMultiJoyCaption2(BizyAirJoyCaption2):
RETURN_TYPES = ("STRING",)
FUNCTION = "multi_joycaption"
NODE_DISPLAY_NAME = "☁️BizyAir Multi Joy Caption"
def multi_joycaption(self, image, **kwargs):
captions = []
images = image
steps = len(images)
pbar = comfy.utils.ProgressBar(steps)
for i, image in enumerate(images):
result = super().joycaption2(image=image.unsqueeze(0), **kwargs)
captions.append(result[0])
pbar.update_absolute(i + 1)
combined_caption = " | ".join(captions)
return {"ui": {"text": (combined_caption,)}, "result": (combined_caption,)}
class SaveCaptionsAndImages:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"captions": ("STRING", {"multiline": True}),
"images": ("IMAGE",),
"directory_prefix": (
"STRING",
{"default": "lora_dataset", "multiline": False},
),
},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "apply"
def apply(self, captions, images, directory_prefix):
# Split the captions string into a list using " | " as the delimiter
caption_list = captions.split(" | ")
full_output_folder = folder_paths.get_output_directory()
# Find the next available directory number
i = 0
while True:
dir_path = os.path.join(full_output_folder, f"{directory_prefix}_{i:03d}")
if not os.path.exists(dir_path):
break
i += 1
# Validate input
if len(caption_list) != len(images):
raise ValueError(
"The number of captions does not match the number of images."
)
for batch_number, (image, caption) in enumerate(zip(images, caption_list)):
# Generate a unique filename for each image
filename = f"image_{batch_number:04d}"
# Generate file paths
image_filepath = os.path.join(dir_path, f"{filename}.png")
caption_filepath = os.path.join(dir_path, f"{filename}.txt")
# Ensure directory exists
os.makedirs(dir_path, exist_ok=True)
# Save the image
i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
img.save(image_filepath)
# Write caption to file
with open(caption_filepath, "w", encoding="utf-8") as caption_file:
caption_file.write(caption)
print(f"Image saved to: {image_filepath}")
print(f"Caption saved to: {caption_filepath}")
return {}
NODE_CLASS_MAPPINGS = {
"BizyAirSiliconCloudLLMAPI": SiliconCloudLLMAPI,
"BizyAirSiliconCloudVLMAPI": SiliconCloudVLMAPI,
"BizyAirJoyCaption": BizyAirJoyCaption,
"BizyAirJoyCaption2": BizyAirJoyCaption2,
"BizyAirMultiJoyCaption2": BizyAirMultiJoyCaption2,
"SaveCaptionsAndImages": SaveCaptionsAndImages,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BizyAirSiliconCloudLLMAPI": "☁️BizyAir SiliconCloud LLM API",
"BizyAirSiliconCloudVLMAPI": "☁️BizyAir SiliconCloud VLM API",
"BizyAirJoyCaption": "☁️BizyAir Joy Caption",
"BizyAirJoyCaption2": "☁️BizyAir Joy Caption2",
"BizyAirMultiJoyCaption2": "☁️BizyAir Multi Joy Caption2",
"SaveCaptionsAndImages": "☁️BizyAir Save Captions And Images",
}