diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..944a683 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.py[cod] +*$py.class +.idea/ +.vscode/ +config/*-config.json +!config/*-config.example.json diff --git a/config/comfyui_rn_translator-config.example.json b/config/comfyui_rn_translator-config.example.json new file mode 100644 index 0000000..c97197d --- /dev/null +++ b/config/comfyui_rn_translator-config.example.json @@ -0,0 +1,29 @@ +{ + "__comment": "RN翻译器配置文件 - 示例", + "llm": { + "current_provider": "quchi", + "providers": { + "quchi": { + "model": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "base_url": "https://llm-oneapi.bytebroad.com/v1", + "api_key": "YOUR_API_KEY_HERE", + "temperature": 0.7, + "max_tokens": 1000, + "top_p": 0.9 + } + } + }, + "vllm": { + "current_provider": "quchi", + "providers": { + "quchi": { + "model": "Qwen/Qwen2.5-32B-Instruct", + "base_url": "https://llm-oneapi.bytebroad.com/v1", + "api_key": "YOUR_API_KEY_HERE", + "temperature": 0.7, + "max_tokens": 196608, + "top_p": 0.9 + } + } + } +} \ No newline at end of file diff --git a/prompter.py b/prompter.py index 2291132..4d2ec5a 100644 --- a/prompter.py +++ b/prompter.py @@ -11,7 +11,6 @@ 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') @@ -23,29 +22,59 @@ def get_config(): config_path = path break - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) + config = {} + if config_path and os.path.exists(config_path): + 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 {} + provider_config = providers.get(current_provider, {}) + + def _env(names): + for n in names: + v = os.environ.get(n) + if v is not None and str(v).strip() != "": + return v + return None + + def _env_float(names, default): + v = _env(names) + if v is None: + return default + try: + return float(v) + except Exception: + return default + + def _env_int(names, default): + v = _env(names) + if v is None: + return default + try: + return int(v) + except Exception: + return default + + # 统一的 API Key 读取逻辑 (Env > Config) + api_key = _env(["COMFYUI_RN_API_KEY", "COMFLY_API_KEY", "RUNNODE_API_KEY", "RN_API_KEY", "LLM_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY"]) or provider_config.get('api_key', '') + base_url = _env(["COMFYUI_RN_BASE_URL", "COMFLY_BASE_URL", "RUNNODE_BASE_URL", "RN_BASE_URL", "LLM_API_BASEURL", "OPENAI_BASE_URL", "OPENAI_API_BASE_URL", "DEEPSEEK_API_BASE_URL"]) or provider_config.get('base_url', '') + model = _env(["COMFYUI_RN_MODEL", "COMFLY_MODEL", "RUNNODE_MODEL", "RN_MODEL", "LLM_MODEL", "OPENAI_MODEL", "DEEPSEEK_MODEL"]) or provider_config.get('model', '') + + temperature = _env_float(["COMFYUI_RN_TEMPERATURE", "COMFLY_TEMPERATURE"], provider_config.get('temperature', 0.7)) + max_tokens = _env_int(["COMFYUI_RN_MAX_TOKENS", "COMFLY_MAX_TOKENS"], provider_config.get('max_tokens', 1000)) + top_p = _env_float(["COMFYUI_RN_TOP_P", "COMFLY_TOP_P"], provider_config.get('top_p', 0.9)) + + return { + 'api_key': api_key, + 'model': model, + 'base_url': base_url, + 'temperature': temperature, + 'max_tokens': max_tokens, + 'top_p': top_p + } except Exception as e: print(f"Error loading config: {e}") @@ -53,47 +82,9 @@ def get_config(): 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}") + # 禁用配置保存以防止密钥泄露 + # raise PermissionError("save_config is disabled") + pass def get_vllm_config(): @@ -108,22 +99,55 @@ def get_vllm_config(): if os.path.exists(path): config_path = path break - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) + config = {} + if config_path and os.path.exists(config_path): + 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 {} + provider_config = providers.get(current_provider, {}) + + def _env(names): + for n in names: + v = os.environ.get(n) + if v is not None and str(v).strip() != "": + return v + return None + + def _env_float(names, default): + v = _env(names) + if v is None: + return default + try: + return float(v) + except Exception: + return default + + def _env_int(names, default): + v = _env(names) + if v is None: + return default + try: + return int(v) + except Exception: + return default + + api_key = _env(["COMFYUI_RN_API_KEY", "COMFLY_API_KEY", "RUNNODE_API_KEY", "RN_API_KEY", "LLM_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY"]) or provider_config.get('api_key', '') + base_url = _env(["COMFYUI_RN_BASE_URL", "COMFLY_BASE_URL", "RUNNODE_BASE_URL", "RN_BASE_URL", "LLM_API_BASEURL", "OPENAI_BASE_URL", "OPENAI_API_BASE_URL", "DEEPSEEK_API_BASE_URL"]) or provider_config.get('base_url', '') + model = _env(["COMFYUI_RN_MODEL", "COMFLY_MODEL", "RUNNODE_MODEL", "RN_MODEL", "LLM_MODEL", "OPENAI_MODEL", "DEEPSEEK_MODEL"]) or provider_config.get('model', '') + temperature = _env_float(["COMFYUI_RN_TEMPERATURE", "COMFLY_TEMPERATURE"], provider_config.get('temperature', 0.7)) + max_tokens = _env_int(["COMFYUI_RN_MAX_TOKENS", "COMFLY_MAX_TOKENS"], provider_config.get('max_tokens', 1000)) + top_p = _env_float(["COMFYUI_RN_TOP_P", "COMFLY_TOP_P"], provider_config.get('top_p', 0.9)) + + return { + 'api_key': api_key, + 'model': model, + 'base_url': base_url, + 'temperature': temperature, + 'max_tokens': max_tokens, + 'top_p': top_p + } except Exception: return {} @@ -311,30 +335,6 @@ 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 { @@ -344,6 +344,9 @@ class RN_Translator(): }, "optional": { "seed": ("INT", {"default": 100, "min": 0, "max": 0xffffffffffffffff}), + "apiBaseUrl": ("STRING", {"default": ""}), + "apiKey": ("STRING", {"default": ""}), + "model": ("STRING", {"default": ""}), } } @@ -387,24 +390,25 @@ class RN_Translator(): 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") + + # 处理输入回退逻辑: Input -> Env -> Config + # 如果输入了有效的apiKey (非空且非default),则使用输入值,否则使用配置值 + if apiKey and str(apiKey).strip() and str(apiKey) != "default": + used_api_key = apiKey + else: + used_api_key = cfg.get("api_key", "") + + if apiBaseUrl and str(apiBaseUrl).strip() and str(apiBaseUrl) != "default": + used_api_baseurl = apiBaseUrl + else: + used_api_baseurl = cfg.get("base_url") or "https://ai.t8star.cn//v1" + + if model and str(model).strip() and str(model) != "default": + used_model = model + else: + used_model = cfg.get("model") or "gpt-4o-mini" - 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密钥" @@ -424,20 +428,28 @@ class RN_Translator(): {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt.strip()}, ] + + cfg_temperature = cfg.get("temperature") use_temperature = ( temperature if temperature is not None else ( cfg_temperature if cfg_temperature is not None else 0.3 ) ) + + cfg_max_tokens = cfg.get("max_tokens") 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, } + + cfg_top_p = cfg.get("top_p") 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() @@ -445,18 +457,19 @@ class RN_Translator(): except Exception as e: return f"翻译错误:{str(e)}" - def translate_text(self, prompt, advanced_options, seed=0): + def translate_text(self, prompt, advanced_options, seed=0, 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 ("错误:请输入要翻译的文本",) 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) + res = self._translate_chunk(chunks[0], src_label, dst_label, temperature=None, apiBaseUrl=apiBaseUrl, apiKey=apiKey, model=model) return (res,) translated = [] for c in chunks: - translated.append(self._translate_chunk(c, src_label, dst_label)) + translated.append(self._translate_chunk(c, src_label, dst_label, temperature=None, apiBaseUrl=apiBaseUrl, apiKey=apiKey, model=model)) return (' '.join(translated),) @classmethod @@ -468,31 +481,6 @@ 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 { @@ -502,6 +490,9 @@ class RN_Prompt_Translator(): }, "optional": { "seed": ("INT", {"default": 100, "min": 0, "max": 0xffffffffffffffff}), + "apiBaseUrl": ("STRING", {"default": ""}), + "apiKey": ("STRING", {"default": ""}), + "model": ("STRING", {"default": ""}), } } @@ -545,24 +536,24 @@ class RN_Prompt_Translator(): 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") + + # 处理输入回退逻辑: Input -> Env -> Config + if apiKey and str(apiKey).strip() and str(apiKey) != "default": + used_api_key = apiKey + else: + used_api_key = cfg.get("api_key", "") + + if apiBaseUrl and str(apiBaseUrl).strip() and str(apiBaseUrl) != "default": + used_api_baseurl = apiBaseUrl + else: + used_api_baseurl = cfg.get("base_url") or "https://ai.t8star.cn//v1" + + if model and str(model).strip() and str(model) != "default": + used_model = model + else: + used_model = cfg.get("model") or "gpt-4o-mini" - 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密钥" @@ -583,20 +574,28 @@ class RN_Prompt_Translator(): {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt.strip()}, ] + + cfg_temperature = cfg.get("temperature") use_temperature = ( temperature if temperature is not None else ( cfg_temperature if cfg_temperature is not None else 0.3 ) ) + + cfg_max_tokens = cfg.get("max_tokens") 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, } + + cfg_top_p = cfg.get("top_p") 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() @@ -604,18 +603,19 @@ class RN_Prompt_Translator(): except Exception as e: return f"翻译错误:{str(e)}" - def translate_text(self, prompt, advanced_options, seed=0): + def translate_text(self, prompt, advanced_options, seed=0, 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 ("错误:请输入要翻译的文本",) 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) + res = self._translate_chunk(chunks[0], src_label, dst_label, temperature=None, apiBaseUrl=apiBaseUrl, apiKey=apiKey, model=model) return (res,) translated = [] for c in chunks: - translated.append(self._translate_chunk(c, src_label, dst_label)) + translated.append(self._translate_chunk(c, src_label, dst_label, temperature=None, apiBaseUrl=apiBaseUrl, apiKey=apiKey, model=model)) return (' '.join(translated),) @classmethod @@ -627,31 +627,6 @@ 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 { @@ -661,9 +636,9 @@ class RN_Midjourney_Prompter(): "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"}), + "apiBaseUrl": ("STRING", {"default": "default"}), + "apiKey": ("STRING", {"default": "default"}), + "model": ("STRING", {"default": "default"}), } } @@ -698,38 +673,35 @@ class RN_Midjourney_Prompter(): 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") + + # 处理输入回退逻辑: Input -> Env -> Config + if apiKey and str(apiKey).strip() and str(apiKey) != "default": + used_api_key = apiKey + else: + used_api_key = cfg.get("api_key", "") - 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 apiBaseUrl and str(apiBaseUrl).strip() and str(apiBaseUrl) != "default": + used_api_baseurl = apiBaseUrl + else: + used_api_baseurl = cfg.get("base_url") or "https://ai.t8star.cn//v1" + + if model and str(model).strip() and str(model) != "default": + used_model = model + else: + used_model = cfg.get("model") or "gpt-4o-mini" + if not used_api_key: return "错误:请提供API密钥" try: client = OpenAI(api_key=used_api_key, base_url=used_api_baseurl) - system_prompt = "你是资深提示词工程师,专注将输入内容重写为 Midjourney 风格英文提示词。" + system_prompt = "You are a professional Midjourney prompt engineer." 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 + Rewrite the following description into a high-quality Midjourney prompt. + - Format: [Subject], [Action/Context], [Art Style/Medium], [Lighting], [Colors], [Composition], --ar 16:9 --v 6.0 + - Keep it descriptive but concise. + - Use English only. Content: {chunk} """ @@ -737,26 +709,34 @@ class RN_Midjourney_Prompter(): {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt.strip()}, ] + + cfg_temperature = cfg.get("temperature") use_temperature = ( temperature if temperature is not None else ( cfg_temperature if cfg_temperature is not None else 0.3 ) ) + + cfg_max_tokens = cfg.get("max_tokens") 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, } + + cfg_top_p = cfg.get("top_p") 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返回空结果" + return "Error: Empty response" except Exception as e: - return f"生成错误:{str(e)}" + return f"Error: {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 "") @@ -913,6 +893,8 @@ class RN_LLMAPI_Pro_Node(): }, "optional": { "ref_image": ("IMAGE",), + "api_baseurl": ("STRING", {"multiline": True, "default": ""}), + "api_key": ("STRING", {"multiline": True, "default": ""}), } } @@ -1011,4 +993,4 @@ class RN_LLMAPI_Pro_Node(): print(f"API call error: {e}") prompt = f'Error: {str(e)}' - return (prompt,) \ No newline at end of file + return (prompt,)