diff --git a/README.md b/README.md index fd96e40..6552c04 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ## 1. 项目介绍 -ComfyUI-DeepSeek-Toolkit 是一个 ComfyUI 的自定义节点包,旨在帮助用户利用来自中国供应商的强大 AI 大语言模型 (LLM),例如 DeepSeek、Qwen 等。该工具包致力于让更广泛的受众能够访问这些先进的模型,即使是那些计算资源有限的用户。 +ComfyUI-DeepSeek-Toolkit 是一个 ComfyUI 的自定义节点包,旨在帮助用户利用来自中国公司的强大 AI 大语言模型 (LLM),例如 DeepSeek、Qwen 等。该工具包致力于让更广泛的受众能够访问这些先进的模型,即使是那些计算资源有限的用户。 该项目的灵感来自于 DeepSeek 和 Qwen 等令人印象深刻的 LLM 的出现。它旨在通过提供轻量级和 API 驱动的解决方案,将这些前沿 AI 技术与日常用户连接起来,从而将这些模型集成到 ComfyUI 工作流程中。 -ComfyUI-DeepSeek-Toolkit 的使命是使开发人员和创意人员能够在现成的硬件(如旧笔记本电脑)上完成通常需要大量计算能力的任务。该项目设想了一个 AI 民主化并为所有人所用的未来。 +ComfyUI-DeepSeek-Toolkit 的使命是使开发人员和创意人员能够在现成的硬件(如旧笔记本电脑)上完成通常需要大量计算能力的任务。 ## 2. 功能特性 diff --git a/__init__.py b/__init__.py index 973a3d6..37f8dfa 100644 --- a/__init__.py +++ b/__init__.py @@ -4,6 +4,7 @@ from .py.image_understanding import NODE_CLASS_MAPPINGS as IMAGE_UNDERSTANDING_M from .py.model_loader import NODE_CLASS_MAPPINGS as MODEL_LOADER_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as MODEL_LOADER_DISPLAY_MAPPINGS from .py.openai_compatible_loader import NODE_CLASS_MAPPINGS as OPENAI_COMPATIBLE_LOADER_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as OPENAI_COMPATIBLE_LOADER_DISPLAY_MAPPINGS from .py.image_preprocessor import NODE_CLASS_MAPPINGS as IMAGE_PREPROCESSOR_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as IMAGE_PREPROCESSOR_DISPLAY_MAPPINGS +from .py.VideoFileUploader import NODE_CLASS_MAPPINGS as VIDEO_FILE_UPLOADER_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as VIDEO_FILE_UPLOADER_DISPLAY_MAPPINGS # 新增导入 # 合并所有节点映射 NODE_CLASS_MAPPINGS = { @@ -12,7 +13,8 @@ NODE_CLASS_MAPPINGS = { **IMAGE_UNDERSTANDING_MAPPINGS, **MODEL_LOADER_MAPPINGS, **OPENAI_COMPATIBLE_LOADER_MAPPINGS, - **IMAGE_PREPROCESSOR_MAPPINGS + **IMAGE_PREPROCESSOR_MAPPINGS, + **VIDEO_FILE_UPLOADER_MAPPINGS # 新增合并 } NODE_DISPLAY_NAME_MAPPINGS = { @@ -21,7 +23,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { **IMAGE_UNDERSTANDING_DISPLAY_MAPPINGS, **MODEL_LOADER_DISPLAY_MAPPINGS, **OPENAI_COMPATIBLE_LOADER_DISPLAY_MAPPINGS, - **IMAGE_PREPROCESSOR_DISPLAY_MAPPINGS + **IMAGE_PREPROCESSOR_DISPLAY_MAPPINGS, + **VIDEO_FILE_UPLOADER_DISPLAY_MAPPINGS # 新增合并 } WEB_DIRECTORY = "./web" diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc index ee2cf67..1566272 100644 Binary files a/__pycache__/__init__.cpython-312.pyc and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/google_file_api.cpython-312.pyc b/__pycache__/google_file_api.cpython-312.pyc new file mode 100644 index 0000000..35d4683 Binary files /dev/null and b/__pycache__/google_file_api.cpython-312.pyc differ diff --git a/google_file_api.py b/google_file_api.py new file mode 100644 index 0000000..1abaf48 --- /dev/null +++ b/google_file_api.py @@ -0,0 +1,79 @@ +import os +import io +import base64 +from typing import Tuple, Optional +import google.generativeai as genai # 导入 Gemini API +import time +import folder_paths +import json + +class GoogleDriveUpload: + """ + A ComfyUI node for uploading video files to Gemini via File API. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "api_key": ("STRING", {"multiline": False, "default": ""}), # API 密钥字符串 + "file_path": ("STRING", {"default": "", "label": "Video File Path"}), + "file_name": ("STRING", {"default": "", "label": "File Name"}), + # "mime_type": (["video/mp4", "video/avi", "video/quicktime", "video/x-matroska"], {"default": "video/mp4", "label": "MIME Type"}), # Gemini File API 不需要mime_type + # "folder_id": ("STRING", {"default": "", "label": "Google Drive Folder ID", "description": "Optional: If empty, uploads to root."}), # Gemini File API 不需要folder_id + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("file_uri",) + FUNCTION = "upload_file" + CATEGORY = "DeepSeek_Toolkit/Google Drive" + + def upload_file(self, api_key: str, file_path: str, file_name: str) -> Tuple[str]: + """ + Uploads a file to Gemini via File API. + + Args: + api_key (str): The Gemini API key. + file_path (str): The path to the file to upload. + file_name (str): The desired name for the file on Gemini. + + Returns: + str: The URI of the uploaded file. + """ + try: + # 1. 验证 API 密钥 + if not api_key: + raise ValueError("API 密钥不能为空") + + # 2. 验证文件路径 + if not os.path.exists(file_path): + raise FileNotFoundError(f"文件未找到: {file_path}") + + # 3. 构建 Gemini API 客户端 + genai.configure(api_key=api_key) + model = genai.GenerativeModel(model_name="gemini-1.5-pro") # 假设使用 gemini-1.5-pro 模型 + client = model + + # 4. 上传文件 + print("Uploading file...") + video_file = client.upload(path=file_path, name=file_name) + file_uri = video_file.uri + print(f"Completed upload: {file_uri}") + + return (file_uri,) + + except Exception as e: + print(f"上传文件时发生错误: {e}") + return (f"Error: {str(e)}",) + + +# 节点类映射 +NODE_CLASS_MAPPINGS = { + "GoogleDriveUpload": GoogleDriveUpload +} + +# 节点显示名称映射 +NODE_DISPLAY_NAME_MAPPINGS = { + "GoogleDriveUpload": "Google Drive Upload (Gemini File API)" +} \ No newline at end of file diff --git a/py/VideoFileUploader.py b/py/VideoFileUploader.py new file mode 100644 index 0000000..8586110 --- /dev/null +++ b/py/VideoFileUploader.py @@ -0,0 +1,44 @@ +import folder_paths +import nodes +import os +from typing import Tuple + +class VideoFileUploader: + """ + Custom node for uploading video files to Gemini API + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video_path": ("STRING", {"default": "", "label": "Video File Path"}), + "api_key": ("STRING", {"default": "", "label": "Gemini API Key"}), # 模拟API Key输入 + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_url",) + FUNCTION = "upload_video" + CATEGORY = "DeepSeek_Toolkit" + + def upload_video(self, video_path: str, api_key: str) -> Tuple[str]: + """ + Uploads a video file to the Gemini API using the File API. + """ + # 模拟文件上传 + if not os.path.exists(video_path): + raise FileNotFoundError(f"Video file not found: {video_path}") + + # 模拟上传过程 + print(f"[INFO] 上传视频文件: {video_path}") + # 模拟生成视频URL + video_url = f"gemini://file/{os.path.basename(video_path)}" # 模拟视频URL + print(f"[INFO] 视频上传完成,URL: {video_url}") + + return (video_url,) + + +# 注册节点 +NODE_CLASS_MAPPINGS = {"VideoFileUploader": VideoFileUploader} +NODE_DISPLAY_NAME_MAPPINGS = {"VideoFileUploader": "Video File Uploader"} \ No newline at end of file diff --git a/py/__init__.py b/py/__init__.py index 40cfb25..977aad5 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -3,4 +3,5 @@ from . import image_generation from . import image_understanding from . import model_loader from . import openai_compatible_loader -from . import image_preprocessor \ No newline at end of file +from . import image_preprocessor +from . import VideoFileUploader # 新增导入 \ No newline at end of file diff --git a/py/__pycache__/VideoFileUploader.cpython-312.pyc b/py/__pycache__/VideoFileUploader.cpython-312.pyc new file mode 100644 index 0000000..164bc47 Binary files /dev/null and b/py/__pycache__/VideoFileUploader.cpython-312.pyc differ diff --git a/py/__pycache__/__init__.cpython-312.pyc b/py/__pycache__/__init__.cpython-312.pyc index 1806aa9..df3368a 100644 Binary files a/py/__pycache__/__init__.cpython-312.pyc and b/py/__pycache__/__init__.cpython-312.pyc differ diff --git a/py/__pycache__/model_loader.cpython-312.pyc b/py/__pycache__/model_loader.cpython-312.pyc index 92baec6..7056601 100644 Binary files a/py/__pycache__/model_loader.cpython-312.pyc and b/py/__pycache__/model_loader.cpython-312.pyc differ diff --git a/py/__pycache__/openai_compatible_loader.cpython-312.pyc b/py/__pycache__/openai_compatible_loader.cpython-312.pyc index 8bb799d..1c6ecc5 100644 Binary files a/py/__pycache__/openai_compatible_loader.cpython-312.pyc and b/py/__pycache__/openai_compatible_loader.cpython-312.pyc differ diff --git a/py/openai_compatible_loader.py b/py/openai_compatible_loader.py index 498ff37..4436861 100644 --- a/py/openai_compatible_loader.py +++ b/py/openai_compatible_loader.py @@ -27,6 +27,7 @@ class OpenAICompatibleLoader: }, "optional": { "prep_img": ("STRING", {"default": "", "forceInput": True}), + "video_url": ("STRING", {"default": "", "forceInput": True, "label": "Video URL"}), # 新增视频URL输入 "system_prompt": ("STRING", {"default": "你是一个AI大模型", "multiline": True}), "prompt": ("STRING", {"multiline": True}), "temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0}), @@ -70,7 +71,7 @@ class OpenAICompatibleLoader: def generate(self, base_url: str, api_key: str, prompt: str, model: str, temperature: float, - max_tokens: int, system_prompt: Optional[str] = None, prep_img: Optional[str] = None, enable_memory: bool = False): + max_tokens: int, system_prompt: Optional[str] = None, prep_img: Optional[str] = None, video_url: Optional[str] = None, enable_memory: bool = False): # 新增video_url参数 content = [] # 将 content 初始化放在函数的最开始 # 移除图像参数类型的日志 @@ -85,6 +86,9 @@ class OpenAICompatibleLoader: # 将 prep_img 添加到 content 中 content.append({"type": "image_url", "image_url": {"url": prep_img}}) + if video_url: # 如果提供了视频URL,则将其添加到content中 + content.append({"type": "video_url", "video_url": {"url": video_url}}) + if prompt.strip(): content.append({"type": "text", "text": prompt}) @@ -101,7 +105,7 @@ class OpenAICompatibleLoader: "content": content }) # 移除带有图像内容的消息日志 - elif not prompt.strip() and not system_prompt and prep_img is None: # 更精确的判断用户是否提供了有效输入 + elif not prompt.strip() and not system_prompt and prep_img is None and video_url is None: # 更精确的判断用户是否提供了有效输入 raise ValueError("用户输入的 prompt 不能为空") # 模型选择逻辑 (保持不变) @@ -216,6 +220,9 @@ class OpenAICompatibleLoader: else: image_tokens = 0 # 未知类型,默认为 0 input_tokens += image_tokens + if video_url: + video_tokens = len(video_url) // 1000 # 估算token + input_tokens += video_tokens try: time.sleep(1)