Files
SF-Runnode-ComfyUI_rn_prompter/prompter.py
T
2025-12-13 19:50:21 +08:00

1014 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from openai import OpenAI
import time
import json
import os
import re
from PIL import Image
import numpy as np
import base64
def get_config():
try:
# 修改配置文件路径为新的配置文件
config_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
config_path = None
for path in config_paths:
if os.path.exists(path):
config_path = path
break
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# 从llm部分获取当前provider的配置
llm_config = config.get('llm', {})
current_provider = llm_config.get('current_provider', 'quchi')
providers = llm_config.get('providers', {})
# 获取当前provider的配置
if current_provider in providers:
provider_config = providers[current_provider]
# 返回包含api_key的字典,保持与原函数兼容
return {
'api_key': provider_config.get('api_key', ''),
'model': provider_config.get('model', ''),
'base_url': provider_config.get('base_url', ''),
'temperature': provider_config.get('temperature', 0.7),
'max_tokens': provider_config.get('max_tokens', 1000),
'top_p': provider_config.get('top_p', 0.9)
}
else:
# 如果当前provider不存在,返回空字典
return {}
except Exception as e:
print(f"Error loading config: {e}")
return {}
def save_config(config):
try:
# 修改配置文件路径为新的配置文件
config_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
config_path = None
for path in config_paths:
if os.path.exists(path):
config_path = path
break
# 先读取现有的配置
with open(config_path, 'r', encoding='utf-8') as f:
existing_config = json.load(f)
# 从llm部分获取当前provider
llm_config = existing_config.get('llm', {})
current_provider = llm_config.get('current_provider', 'deepseek')
providers = llm_config.get('providers', {})
# 更新当前provider的api_key
if current_provider in providers:
# 只更新api_key,保持其他配置不变
if 'api_key' in config:
providers[current_provider]['api_key'] = config['api_key']
# 可选:更新其他字段
for key in ['model', 'base_url', 'temperature', 'max_tokens', 'top_p']:
if key in config:
providers[current_provider][key] = config[key]
# 更新回配置
existing_config['llm']['providers'] = providers
# 写回文件
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(existing_config, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving config: {e}")
def get_vllm_config():
try:
config_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
config_path = None
for path in config_paths:
if os.path.exists(path):
config_path = path
break
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
vllm_config = config.get('vllm', {})
current_provider = vllm_config.get('current_provider', 'quchi')
providers = vllm_config.get('providers', {})
if current_provider in providers:
provider_config = providers[current_provider]
return {
'api_key': provider_config.get('api_key', ''),
'model': provider_config.get('model', ''),
'base_url': provider_config.get('base_url', ''),
'temperature': provider_config.get('temperature', 0.7),
'max_tokens': provider_config.get('max_tokens', 1000),
'top_p': provider_config.get('top_p', 0.9)
}
return {}
except Exception:
return {}
def get_model_config(model_name):
"""
根据模型显示名称查找配置文件中的配置
Args:
model_name: 用户在界面中选择的模型名称(如"Qwen2.5-VL-32B-Instruct")
Returns:
包含该模型配置的字典,如果找不到则返回空字典
"""
try:
# 修改配置文件路径为新的配置文件
config_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
config_path = None
for path in config_paths:
if os.path.exists(path):
config_path = path
break
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# 模型名称映射字典(前端显示名称 -> 配置中的模型名称)
mapping = {
"Qwen2.5-32B-Instruct": "Qwen/Qwen2.5-32B-Instruct",
"Qwen2.5-72B-Instruct": "Qwen/Qwen2.5-72B-Instruct",
"Qwen2.5-VL-32B-Instruct": "Qwen/Qwen2.5-VL-32B-Instruct",
"Qwen2.5-Omni-7B": "Qwen/Qwen2.5-Omni-7B",
"Qwen3-32B": "Qwen/Qwen3-32B",
"Qwen3-Embedding-8B": "Qwen/Qwen3-Embedding-8B",
"Qwen3-Reranker-8B": "Qwen/Qwen3-Reranker-8B",
"Qwen3-Coder-480B-A35B-Instruct": "Qwen/Qwen3-Coder-480B-A35B-Instruct",
"DeepSeek-R1-Distill-Qwen-32B": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
"DeepSeek-R1-0528": "deepseek-ai/DeepSeek-R1-0528",
"DeepSeek-V3-0324": "deepseek-ai/DeepSeek-V3-0324",
"glm-4-9b-chat": "glm-4-9b-chat",
"GLM-4-32B-0414": "ZhipuAI/GLM-4-32B-0414",
"GLM-4.1V-9B-Thinking": "ZhipuAI/GLM-4.1V-9B-Thinking",
"GLM-4.6": "ZhipuAI/GLM-4.6",
"bge-m3": "bge-m3",
"bge-reranker-v2-m3": "bge-reranker-v2-m3",
}
# 获取映射后的模型名称
mapped_name = mapping.get(model_name, model_name)
# 直接在整个配置中查找匹配的model字段
def find_matching_config(config_dict, target_model):
"""递归查找匹配的配置"""
if isinstance(config_dict, dict):
# 检查当前字典是否有model字段且匹配
if 'model' in config_dict and config_dict['model'] == target_model:
# 找到了匹配的配置
return {
'api_key': config_dict.get('api_key', ''),
'model': config_dict.get('model', ''),
'base_url': config_dict.get('base_url', ''),
'temperature': config_dict.get('temperature', 0.7),
'max_tokens': config_dict.get('max_tokens', 1000),
'top_p': config_dict.get('top_p', 0.9)
}
# 递归查找子字典
for key, value in config_dict.items():
result = find_matching_config(value, target_model)
if result:
return result
elif isinstance(config_dict, list):
# 递归查找列表中的字典
for item in config_dict:
result = find_matching_config(item, target_model)
if result:
return result
return None
# 在整个配置中查找
result = find_matching_config(config, mapped_name)
if result:
print(f"Found config for {model_name} -> {mapped_name}")
return result
print(f"No matching config found for {model_name} -> {mapped_name}")
return {}
except Exception as e:
print(f"Error loading model config: {e}")
return {}
baseurl = get_config().get('base_url', '')
def encode_image_b64(ref_image):
i = 255. * ref_image.cpu().numpy()[0]
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
lsize = np.max(img.size)
factor = 1
while lsize / factor > 2048:
factor *= 2
img = img.resize((img.size[0] // factor, img.size[1] // factor))
image_path = f'{time.time()}.webp'
img.save(image_path, 'WEBP')
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
os.remove(image_path)
return base64_image
def extract_video_frames(video_tensor, max_frames=20):
"""
从视频张量中提取关键帧
Args:
video_tensor: 视频张量,形状为 (frames, height, width, channels)
max_frames: 最大提取帧数
Returns:
List[base64]: 编码后的图像列表
"""
import torch
frames = []
total_frames = video_tensor.shape[0]
# 计算采样间隔,均匀采样关键帧
if total_frames <= max_frames:
indices = range(total_frames)
else:
step = total_frames / max_frames
indices = [int(i * step) for i in range(max_frames)]
for idx in indices:
# 获取单帧图像
frame = video_tensor[idx]
# 转换为合适的格式 (height, width, channels)
if isinstance(frame, torch.Tensor):
frame_np = frame.cpu().numpy()
else:
frame_np = frame
# 确保值在0-255范围
if frame_np.max() <= 1.0:
frame_np = frame_np * 255.0
# 转换为uint8
frame_img = Image.fromarray(np.clip(frame_np, 0, 255).astype(np.uint8))
# 调整大小(与图片处理保持一致)
lsize = np.max(frame_img.size)
factor = 1
while lsize / factor > 1024: # 视频帧可以稍小一些
factor *= 2
if factor > 1:
frame_img = frame_img.resize((frame_img.size[0] // factor, frame_img.size[1] // factor))
# 保存为临时文件并编码
frame_path = f'frame_{time.time()}_{idx}.webp'
frame_img.save(frame_path, 'WEBP')
with open(frame_path, "rb") as f:
frame_base64 = base64.b64encode(f.read()).decode('utf-8')
frames.append(frame_base64)
os.remove(frame_path)
return frames
class RN_Translator():
def __init__(self):
pass
def _load_llm_config(self):
cfg_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
cfg_path = None
for path in cfg_paths:
if os.path.exists(path):
cfg_path = path
break
if not os.path.exists(cfg_path):
return {}
try:
with open(cfg_path, "r", encoding="utf-8") as f:
data = json.load(f)
llm = data.get("llm") or {}
current = llm.get("current_provider")
providers = llm.get("providers") or {}
provider_cfg = providers.get(current) or {}
return provider_cfg
except Exception:
return {}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": "prompt"}),
"advanced_options": (["auto", "force_english", "force_chinese"], {"default": "auto"}),
},
"optional": {
"seed": ("INT", {"default": 100, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("translation",)
FUNCTION = "translate_text"
CATEGORY = "RunNode/rn_prompter"
TITLE = "RunNode Translator"
def _split_text_into_chunks(self, text, max_chunk_size=400):
if len(text) <= max_chunk_size:
return [text]
sentences = re.split(r'[.!?。!?]\s*', text)
chunks = []
current = ""
for s in sentences:
if len(current + s) <= max_chunk_size:
current += (s + ". " if s else "")
else:
if current:
chunks.append(current.strip())
current = (s + ". " if s else "")
if current:
chunks.append(current.strip())
final = []
for c in chunks:
if len(c) <= max_chunk_size:
final.append(c)
else:
for i in range(0, len(c), max_chunk_size):
final.append(c[i:i + max_chunk_size])
return final
def _detect_direction(self, text, advanced_options):
if advanced_options == "force_English":
return ("源语言", "English")
if advanced_options == "force_Chinese":
return ("source", "Chinese")
if re.search(r"[\u4e00-\u9fff]", text):
return ("中文", "English")
return ("English", "Chinese")
def _translate_chunk(self, chunk, src_label, dst_label, temperature=None, apiBaseUrl=None, apiKey=None, model=None):
if apiBaseUrl == "default":
apiBaseUrl = ""
if apiKey == "default":
apiKey = ""
if model == "default":
model = ""
cfg = get_config()
cfg_base_url = cfg.get("base_url")
cfg_api_key = cfg.get("api_key")
cfg_model = cfg.get("model")
cfg_temperature = cfg.get("temperature")
cfg_max_tokens = cfg.get("max_tokens")
cfg_top_p = cfg.get("top_p")
used_api_baseurl = (apiBaseUrl or cfg_base_url or "https://ai.t8star.cn//v1")
used_model = (model or cfg_model or "gpt-4o-mini")
used_api_key = (apiKey or cfg_api_key or "")
if not used_api_key:
return "错误:请提供API密钥"
try:
client = OpenAI(api_key=used_api_key, base_url=used_api_baseurl)
system_prompt = "你是一个专业的翻译助手,负责准确翻译文本内容。"
user_prompt = f"""
请将以下{src_label}内容翻译成{dst_label}:
{chunk}
要求:
- 保持原意不变
- 语言自然流畅
- 只返回翻译结果,不要添加任何解释或额外文本
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt.strip()},
]
use_temperature = (
temperature if temperature is not None else (
cfg_temperature if cfg_temperature is not None else 0.3
)
)
use_max_tokens = cfg_max_tokens if cfg_max_tokens is not None else 512
params = {
"model": used_model,
"messages": messages,
"temperature": use_temperature,
"max_tokens": use_max_tokens,
}
if cfg_top_p is not None:
params["top_p"] = cfg_top_p
completion = client.chat.completions.create(**params)
if completion is not None and hasattr(completion, 'choices') and len(completion.choices) > 0:
return completion.choices[0].message.content.strip()
return "错误:API返回空结果"
except Exception as e:
return f"翻译错误:{str(e)}"
def translate_text(self, prompt, advanced_options, seed=0):
cleaned = re.sub(r'[\x00\x01-\x08\x0b\x0c\x0e-\x1f\x7f]', '', prompt or "")
if not cleaned.strip():
return ("错误:请输入要翻译的文本",)
src_label, dst_label = self._detect_direction(cleaned, advanced_options)
chunks = self._split_text_into_chunks(cleaned, max_chunk_size=400)
if len(chunks) == 1:
res = self._translate_chunk(chunks[0], src_label, dst_label)
return (res,)
translated = []
for c in chunks:
translated.append(self._translate_chunk(c, src_label, dst_label))
return (' '.join(translated),)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float(time.time())
class RN_Prompt_Translator():
def __init__(self):
pass
def _load_llm_config(self):
# cfg_path = os.path.join(os.path.dirname(__file__), "config", "ComfyUI_rn_translator-config.json")
cfg_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
cfg_path = None
for path in cfg_paths:
if os.path.exists(path):
cfg_path = path
break
if not os.path.exists(cfg_path):
return {}
try:
with open(cfg_path, "r", encoding="utf-8") as f:
data = json.load(f)
llm = data.get("llm") or {}
current = llm.get("current_provider")
providers = llm.get("providers") or {}
provider_cfg = providers.get(current) or {}
return provider_cfg
except Exception:
return {}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"advanced_options": (["auto", "force_English", "force_Chinese"], {"default": "auto"}),
},
"optional": {
"seed": ("INT", {"default": 100, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("translation",)
FUNCTION = "translate_text"
CATEGORY = "RunNode/rn_prompter"
TITLE = "RunNode Prompt Translator"
def _split_text_into_chunks(self, text, max_chunk_size=400):
if len(text) <= max_chunk_size:
return [text]
sentences = re.split(r'[.!?。!?]\s*', text)
chunks = []
current = ""
for s in sentences:
if len(current + s) <= max_chunk_size:
current += (s + ". " if s else "")
else:
if current:
chunks.append(current.strip())
current = (s + ". " if s else "")
if current:
chunks.append(current.strip())
final = []
for c in chunks:
if len(c) <= max_chunk_size:
final.append(c)
else:
for i in range(0, len(c), max_chunk_size):
final.append(c[i:i + max_chunk_size])
return final
def _detect_direction(self, text, advanced_options):
if advanced_options == "force_English":
return ("源语言", "English")
if advanced_options == "force_Chinese":
return ("source", "Chinese")
if re.search(r"[\u4e00-\u9fff]", text):
return ("中文", "English")
return ("English", "Chinese")
def _translate_chunk(self, chunk, src_label, dst_label, temperature=None, apiBaseUrl=None, apiKey=None, model=None):
if apiBaseUrl == "default":
apiBaseUrl = ""
if apiKey == "default":
apiKey = ""
if model == "default":
model = ""
cfg = get_config()
cfg_base_url = cfg.get("base_url")
cfg_api_key = cfg.get("api_key")
cfg_model = cfg.get("model")
cfg_temperature = cfg.get("temperature")
cfg_max_tokens = cfg.get("max_tokens")
cfg_top_p = cfg.get("top_p")
used_api_baseurl = (apiBaseUrl or cfg_base_url or "https://ai.t8star.cn//v1")
used_model = (model or cfg_model or "gpt-4o-mini")
used_api_key = (apiKey or cfg_api_key or "")
if not used_api_key:
return "错误:请提供API密钥"
try:
client = OpenAI(api_key=used_api_key, base_url=used_api_baseurl)
system_prompt = "你是资深提示词工程师,负责将输入内容重写为用于生成式模型的标准化提示词,保持简洁、具象、可执行。"
user_prompt = f"""
Rewrite the following content as a standard {dst_label} prompt for generative models.
Requirements:
- concise, vivid, comma-separated phrases
- include subject, attributes, composition, style, lighting
- no explanations, headers or extra text
- return only the final prompt in {dst_label}
Content:
{chunk}
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt.strip()},
]
use_temperature = (
temperature if temperature is not None else (
cfg_temperature if cfg_temperature is not None else 0.3
)
)
use_max_tokens = cfg_max_tokens if cfg_max_tokens is not None else 512
params = {
"model": used_model,
"messages": messages,
"temperature": use_temperature,
"max_tokens": use_max_tokens,
}
if cfg_top_p is not None:
params["top_p"] = cfg_top_p
completion = client.chat.completions.create(**params)
if completion is not None and hasattr(completion, 'choices') and len(completion.choices) > 0:
return completion.choices[0].message.content.strip()
return "错误:API返回空结果"
except Exception as e:
return f"翻译错误:{str(e)}"
def translate_text(self, prompt, advanced_options, seed=0):
cleaned = re.sub(r'[\x00\x01-\x08\x0b\x0c\x0e-\x1f\x7f]', '', prompt or "")
if not cleaned.strip():
return ("错误:请输入要翻译的文本",)
src_label, dst_label = self._detect_direction(cleaned, advanced_options)
chunks = self._split_text_into_chunks(cleaned, max_chunk_size=400)
if len(chunks) == 1:
res = self._translate_chunk(chunks[0], src_label, dst_label)
return (res,)
translated = []
for c in chunks:
translated.append(self._translate_chunk(c, src_label, dst_label))
return (' '.join(translated),)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float(time.time())
class RN_Midjourney_Prompter():
def __init__(self):
pass
def _load_llm_config(self):
# cfg_path = os.path.join(os.path.dirname(__file__), "config", "ComfyUI_rn_translator-config.json")
cfg_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'ComfyUI_rn_translator-config.json'),
os.path.join(os.path.dirname(os.path.realpath(__file__)), "config", 'comfyui_rn_translator-config.json')
]
cfg_path = None
for path in cfg_paths:
if os.path.exists(path):
cfg_path = path
break
if not os.path.exists(cfg_path):
return {}
try:
with open(cfg_path, "r", encoding="utf-8") as f:
data = json.load(f)
llm = data.get("llm") or {}
current = llm.get("current_provider")
providers = llm.get("providers") or {}
provider_cfg = providers.get(current) or {}
return provider_cfg
except Exception:
return {}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"seed": ("INT", {"default": 100, "min": 0, "max": 0xffffffffffffffff}),
"temperature": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.1}),
# "apiBaseUrl": ("STRING", {"default": "default"}),
# "apiKey": ("STRING", {"default": "default"}),
# "model": ("STRING", {"default": "default"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt opt",)
FUNCTION = "generate_mj_prompt"
CATEGORY = "RunNode/rn_prompter"
TITLE = "Midjourney Style Prompter"
def _split_text_into_chunks(self, text, max_chunk_size=400):
if len(text) <= max_chunk_size:
return [text]
sentences = re.split(r'[.!?。!?]\s*', text)
chunks = []
current = ""
for s in sentences:
if len(current + s) <= max_chunk_size:
current += (s + ". " if s else "")
else:
if current:
chunks.append(current.strip())
current = (s + ". " if s else "")
if current:
chunks.append(current.strip())
final = []
for c in chunks:
if len(c) <= max_chunk_size:
final.append(c)
else:
for i in range(0, len(c), max_chunk_size):
final.append(c[i:i + max_chunk_size])
return final
def _generate_chunk_mj(self, chunk, temperature=None, apiBaseUrl=None, apiKey=None, model=None):
if apiBaseUrl == "default":
apiBaseUrl = ""
if apiKey == "default":
apiKey = ""
if model == "default":
model = ""
cfg = get_config()
cfg_base_url = cfg.get("base_url")
cfg_api_key = cfg.get("api_key")
cfg_model = cfg.get("model")
cfg_temperature = cfg.get("temperature")
cfg_max_tokens = cfg.get("max_tokens")
cfg_top_p = cfg.get("top_p")
used_api_baseurl = (apiBaseUrl or cfg_base_url or "https://ai.t8star.cn//v1")
used_model = (model or cfg_model or "gpt-4o-mini")
used_api_key = (apiKey or cfg_api_key or "")
if not used_api_key:
return "错误:请提供API密钥"
try:
client = OpenAI(api_key=used_api_key, base_url=used_api_baseurl)
system_prompt = "你是资深提示词工程师,专注将输入内容重写为 Midjourney 风格英文提示词。"
user_prompt = f"""
Rewrite the following content as a Midjourney-style prompt in English only.
Requirements:
- concise, comma-separated tags
- include subject, medium, style, lighting, composition, color palette
- add camera/lens and mood if relevant
- no parameters/flags (e.g., --ar, --v, --s)
- no explanations or extra text, return only the final prompt
Content:
{chunk}
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt.strip()},
]
use_temperature = (
temperature if temperature is not None else (
cfg_temperature if cfg_temperature is not None else 0.3
)
)
use_max_tokens = cfg_max_tokens if cfg_max_tokens is not None else 512
params = {
"model": used_model,
"messages": messages,
"temperature": use_temperature,
"max_tokens": use_max_tokens,
}
if cfg_top_p is not None:
params["top_p"] = cfg_top_p
completion = client.chat.completions.create(**params)
if completion is not None and hasattr(completion, 'choices') and len(completion.choices) > 0:
return completion.choices[0].message.content.strip()
return "错误:API返回空结果"
except Exception as e:
return f"生成错误:{str(e)}"
def generate_mj_prompt(self, prompt, seed=0, temperature=0.3, apiBaseUrl="default", apiKey="default", model="default"):
cleaned = re.sub(r'[\x00\x01-\x08\x0b\x0c\x0e-\x1f\x7f]', '', prompt or "")
if not cleaned.strip():
return ("错误:请输入要生成的文本",)
chunks = self._split_text_into_chunks(cleaned, max_chunk_size=400)
if len(chunks) == 1:
res = self._generate_chunk_mj(chunks[0], temperature=temperature, apiBaseUrl=apiBaseUrl, apiKey=apiKey, model=model)
return (res,)
generated = []
for c in chunks:
generated.append(self._generate_chunk_mj(c, temperature=temperature, apiBaseUrl=apiBaseUrl, apiKey=apiKey, model=model))
return (' '.join(generated),)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float(time.time())
class RN_LLMAPI_Node():
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_baseurl": ("STRING", {"multiline": True}),
"api_key": ("STRING", {"default": ""}),
"model": ("STRING", {"default": ""}),
"role": ("STRING", {"multiline": True, "default": "You are a helpful assistant"}),
"prompt": ("STRING", {"multiline": True, "default": "Hello"}),
"temperature": ("FLOAT", {"default": 0.6}),
"seed": ("INT", {"default": 100}),
# "max_video_frames": ("INT", {"default": 5, "min": 1, "max": 20, "step": 1}),
},
"optional": {
"ref_image": ("IMAGE",),
"video": ("VIDEO",),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("describe",)
FUNCTION = "rn_run_llmapi"
CATEGORY = "RunNode/rn_prompter"
def rn_run_llmapi(self, api_baseurl, api_key, model, role, prompt, temperature, seed,
max_video_frames=5, ref_image=None, video=None):
cfg = get_config()
vllm_cfg = get_vllm_config()
has_visual = (ref_image is not None) or (video is not None)
selected_cfg = vllm_cfg if has_visual else cfg
used_api_baseurl = (api_baseurl or selected_cfg.get("base_url"))
used_api_key = (api_key or selected_cfg.get("api_key") or "")
used_model = (model or selected_cfg.get("model") or ("qwen25-vl-32b-instruct" if has_visual else "gpt-4o-mini"))
client = OpenAI(api_key=used_api_key, base_url=used_api_baseurl)
messages = [{'role': 'system', 'content': f'{role}'}]
user_content = [{"type": "text", "text": f"{prompt}"}]
# 处理图片输入
if ref_image is not None:
base64_image = encode_image_b64(ref_image)
user_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/webp;base64,{base64_image}",
"detail": "high"
}
})
# 处理视频输入
if video is not None:
try:
# 提取视频帧
video_frames = extract_video_frames(video, max_frames=max_video_frames)
# 为每个帧添加图像内容
for i, frame_base64 in enumerate(video_frames):
user_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/webp;base64,{frame_base64}",
"detail": "medium"
}
})
# 如果需要,可以在prompt中说明这些是视频帧
if len(video_frames) > 1:
prompt_addition = f"\nNote: I've provided {len(video_frames)} keyframes from the video. "
user_content[0]["text"] = prompt + prompt_addition
except Exception as e:
print(f"视频处理出错: {str(e)}")
# 可以选择返回错误信息或继续处理
# 构建消息
messages.append({
'role': 'user',
'content': user_content
})
try:
completion = client.chat.completions.create(
model=used_model,
messages=messages,
temperature=temperature,
seed=seed
)
if completion is not None and hasattr(completion, 'choices'):
response = completion.choices[0].message.content
else:
response = 'Error: No response from API'
except Exception as e:
response = f"API调用出错: {str(e)}"
return (response,)
class RN_LLMAPI_Pro_Node():
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (["Qwen2.5-32B-Instruct",
"Qwen2.5-72B-Instruct",
"Qwen2.5-VL-32B-Instruct",
"Qwen2.5-Omni-7B",
"Qwen3-32B",
"Qwen3-Embedding-8B",
"Qwen3-Reranker-8B",
"Qwen3-Coder-480B-A35B-Instruct",
"DeepSeek-R1-Distill-Qwen-32B",
"DeepSeek-R1-0528",
"DeepSeek-V3-0324",
"glm-4-9b-chat",
"GLM-4-32B-0414",
"GLM-4.1V-9B-Thinking",
"GLM-4.6",
"bge-m3",
"bge-reranker-v2-m3",
], {"default": "Qwen2.5-VL-32B-Instruct"}),
"role": ("STRING", {"multiline": True, "default": "You are a helpful assistant"}),
"prompt": ("STRING", {"multiline": True, "default": "Hello"}),
"temperature": ("FLOAT", {"default": 0.6}),
"seed": ("INT", {"default": -1}),
},
"optional": {
"ref_image": ("IMAGE",),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("response",)
FUNCTION = "rn_run_llmapi_pro"
CATEGORY = "RunNode/rn_prompter"
def rn_run_llmapi_pro(self, model, role, prompt, temperature, seed, api_baseurl='', api_key='', ref_image=None):
# 获取模型对应的配置
model_config = get_model_config(model)
# 优先使用传入的api_baseurl和api_key,如果没有则使用配置文件中的
used_api_baseurl = api_baseurl or model_config.get('base_url', '')
used_api_key = api_key or model_config.get('api_key', '')
used_model = model_config.get('model', '') # 使用配置中的模型名称
# 如果配置中没有模型名称,使用映射后的名称
if not used_model:
# 模型名称映射字典
mapping = {
"Qwen2.5-32B-Instruct": "Qwen/Qwen2.5-32B-Instruct",
"Qwen2.5-72B-Instruct": "Qwen/Qwen2.5-72B-Instruct",
"Qwen2.5-VL-32B-Instruct": "Qwen/Qwen2.5-VL-32B-Instruct",
"Qwen2.5-Omni-7B": "Qwen/Qwen2.5-Omni-7B",
"Qwen3-32B": "Qwen/Qwen3-32B",
"Qwen3-Embedding-8B": "Qwen/Qwen3-Embedding-8B",
"Qwen3-Reranker-8B": "Qwen/Qwen3-Reranker-8B",
"Qwen3-Coder-480B-A35B-Instruct": "Qwen/Qwen3-Coder-480B-A35B-Instruct",
"DeepSeek-R1-Distill-Qwen-32B": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
"DeepSeek-R1-0528": "deepseek-ai/DeepSeek-R1-0528",
"DeepSeek-V3-0324": "deepseek-ai/DeepSeek-V3-0324",
"glm-4-9b-chat": "glm-4-9b-chat",
"GLM-4-32B-0414": "ZhipuAI/GLM-4-32B-0414",
"GLM-4.1V-9B-Thinking": "ZhipuAI/GLM-4.1V-9B-Thinking",
"GLM-4.6": "ZhipuAI/GLM-4.6",
"bge-m3": "bge-m3",
"bge-reranker-v2-m3": "bge-reranker-v2-m3",
}
used_model = mapping.get(model, model)
# 如果base_url或api_key为空,使用默认的get_config作为后备
if not used_api_baseurl or not used_api_key:
cfg = get_config()
used_api_baseurl = used_api_baseurl or cfg.get('base_url', '')
used_api_key = used_api_key or cfg.get('api_key', '')
# 最终的检查,确保有base_url
if not used_api_baseurl:
return ("ERROR: No API base URL configured. Please check your configuration.",)
print(f"Using base_url: {used_api_baseurl}")
print(f"Using model: {used_model}")
client = OpenAI(api_key=used_api_key, base_url=used_api_baseurl)
# 处理消息(保持原有的图片处理逻辑不变)
if ref_image is None:
messages = [
{'role': 'system', 'content': f'{role}'},
{'role': 'user', 'content': f'{prompt}'},
]
else:
base64_image = encode_image_b64(ref_image)
messages = [
{'role': 'system', 'content': f'{role}'},
{'role': 'user',
'content': [
{
"type": "text",
"text": f"{prompt}"
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
},
]},
]
# 调用API
try:
completion = client.chat.completions.create(
model=used_model,
messages=messages,
temperature=temperature
)
if completion is not None and hasattr(completion, 'choices'):
prompt = completion.choices[0].message.content
else:
prompt = 'Error: No response from API'
except Exception as e:
print(f"API call error: {e}")
prompt = f'Error: {str(e)}'
return (prompt,)