├── .gitignore ├── README.md ├── __init__.py ├── api_request.py ├── image_post_node.py ├── json_array_iterator.py └── text_prompt_combiner_node.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *.pyo 5 | *.pyd 6 | 7 | # Virtual Environment 8 | venv/ 9 | env/ 10 | ENV/ 11 | 12 | # IDE 13 | .vscode/ 14 | 15 | # Compiled files 16 | *.pyc 17 | 18 | # Logs and databases 19 | *.log 20 | *.sqlite3 21 | 22 | # Dependency directories 23 | lib/ 24 | lib64/ 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI_API_Manager 2 | ``` 3 | # Custom Workflow Nodes for API Integration 4 | 5 | This package provides three custom nodes designed to streamline workflows involving API requests, dynamic text manipulation based on API responses, and image posting to APIs. These nodes are particularly useful for automating interactions with APIs, enhancing text-based workflows with dynamic data, and facilitating image uploads. 6 | 7 | ## Installation 8 | 9 | To use these custom nodes, clone this repository into your `custom_nodes` directory. No external dependencies are required. 10 | 11 | ```bash 12 | git clone https://github.com/CC-BryanOttho/ComfyUI_API_Manager.git custom_nodes 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### API Request Node 18 | 19 | This node performs API requests and processes the responses: 20 | 21 | - **auth_url**: Specify the authentication endpoint of the API. 22 | - **auth_body_text**: Enter the payload for the API authentication request. 23 | - **array_path**: Navigate through the response object to locate the desired object or array, especially useful if it's nested. 24 | 25 | 1. Configure the `auth_url` and `auth_body_text` to authenticate with your target API. 26 | 2. Use the `array_path` to specify the path to the data of interest in the API response. 27 | 28 | ### Text Prompt Combiner Node 29 | 30 | Utilizes API response data for dynamic text manipulation: 31 | 32 | - Dynamically replace text in a template using `$attributeName` syntax to insert values from the API response. 33 | - **id_field_name**: Specify the attribute name whose value is returned as a string, aiding in identifying or referencing objects. 34 | 35 | 1. Insert API response data into a text box, using `$attribute1.attribute2` to reference nested attributes. 36 | 2. The `id_field_name` can be used to extract specific values from the response for further use. 37 | 38 | ### Image Post Node 39 | 40 | Facilitates the posting of images to an API: 41 | 42 | - Requires an image, an object ID as a string, and an API key for authentication. 43 | - **api_url**: The endpoint for image posting, with `$id` used in the URL to dynamically insert the ID. 44 | 45 | 1. Provide the necessary image, API object ID, and API key. 46 | 2. Configure the `api_url`, ensuring to include `$id` where the object ID should be inserted in the URL. 47 | 48 | ## Example Workflow 49 | 50 | Imagine a workflow where you need to fetch data from an API, use part of that data to generate a text prompt, and then post an image related to that prompt to another API endpoint: 51 | 52 | 1. **API Request Node** fetches the desired data. 53 | 2. **Text Prompt Combiner Node** generates a dynamic text prompt based on the fetched data. 54 | 3. **Image Post Node** posts an image using details from the previous nodes. 55 | 56 | ## Additional Information 57 | 58 | These nodes are designed to be flexible and can be adjusted or extended based on specific workflow requirements. For more complex API interactions or additional dynamic capabilities, consider customizing these nodes further. 59 | 60 | ## Contributing 61 | 62 | Contributions are welcome! If you have improvements or bug fixes, please submit a pull request or open an issue. 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .api_request import APIRequestNode 2 | from .image_post_node import PostImageToAPI 3 | from .text_prompt_combiner_node import TextPromptCombinerNode 4 | 5 | # Define the mappings for your custom nodes 6 | NODE_CLASS_MAPPINGS = { 7 | "APIRequestNode": APIRequestNode, 8 | "PostImageToAPI": PostImageToAPI, 9 | "TextPromptCombinerNode": TextPromptCombinerNode, 10 | } 11 | 12 | # Define human-readable names for your nodes (optional) 13 | NODE_DISPLAY_NAME_MAPPINGS = { 14 | "APIRequestNode": "API Request Node", 15 | "PostImageToAPI": "Image Post Node", 16 | "TextPromptCombinerNode": "Text Prompt Combiner Node", 17 | } 18 | 19 | # Export the mappings for use by ComfyUI 20 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] 21 | -------------------------------------------------------------------------------- /api_request.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import requests 3 | import json 4 | 5 | class APIRequestNode: 6 | """ 7 | A custom node for making API requests and processing responses. 8 | 9 | Attributes and class methods are defined to match the framework's expectations, 10 | similar to the provided Example node structure. 11 | """ 12 | @classmethod 13 | def INPUT_TYPES(cls): 14 | """ 15 | Define input parameters for the APIRequestNode. 16 | """ 17 | return { 18 | "required": { 19 | "api_url": ("STRING", {"default": ""}), 20 | "auth_url": ("STRING", {"default": ""}), 21 | "token_attribute_name": ("STRING", {"default": ""}), 22 | "auth_body_text": ("STRING", {"multiline": True}), 23 | "array_path": ("STRING", {"default": ""}), 24 | "iteration_index": ("INT", { 25 | "default": 0, 26 | "min": 0, 27 | "max": 9999, # Adjust based on expected maximum array length 28 | "step": 1, 29 | "display": "number" # Allow users to input or select the index number 30 | }) 31 | } 32 | } 33 | 34 | RETURN_TYPES = ("JSON", "INT", "STRING") # Output types: JSON response 35 | RETURN_NAMES = ("API_RESPONSE", "LENGTH", "API_KEY") 36 | FUNCTION = "execute" # The entry-point method for this node 37 | CATEGORY = "API Manager" # Category under which the node will appear in the UI 38 | 39 | def __init__(self): 40 | pass 41 | 42 | def authenticate(self, auth_url, auth_body, token_attribute_name): 43 | """ 44 | Authenticate and retrieve a bearer token using the provided authentication URL and body. 45 | """ 46 | try: 47 | response = requests.post(auth_url, json=auth_body) 48 | response.raise_for_status() 49 | token = response.json().get(token_attribute_name) 50 | print(f"APIRequestNode: Obtained token") 51 | return token 52 | except requests.exceptions.RequestException as e: 53 | print(f"APIRequestNode: Error obtaining bearer token - {e}") 54 | return None 55 | 56 | def execute(self, api_url, auth_url, token_attribute_name, auth_body_text, array_path, iteration_index): 57 | """ 58 | The main logic for making an API request, processing the response, 59 | and potentially queuing image generation requests or other post-processing steps. 60 | """ 61 | auth_body = None 62 | if auth_body_text: 63 | try: 64 | auth_body = json.loads("{" + auth_body_text + "}") 65 | except json.JSONDecodeError as e: 66 | print(f"Error parsing JSON input: {e}") 67 | return None, None, None 68 | 69 | token = self.authenticate(auth_url, auth_body, token_attribute_name) if auth_url else None 70 | headers = {'Authorization': f'Bearer {token}'} if token else {} 71 | response_data, error = self.make_api_request(api_url, headers) 72 | 73 | if error: 74 | return {}, None, "" 75 | 76 | array_data = self.extract_array_data(response_data, array_path) if array_path else response_data 77 | selected_item = array_data[iteration_index] if isinstance(array_data, list) else array_data 78 | return selected_item, len(array_data), f'Bearer {token}' 79 | 80 | def make_api_request(self, api_url, headers, params={}): 81 | """ 82 | Make the actual API request and return the response. 83 | """ 84 | try: 85 | response = requests.get(api_url, headers=headers, params=params) 86 | response.raise_for_status() 87 | return response.json(), None 88 | except requests.exceptions.RequestException as e: 89 | print(f"Error making API request: {e}") 90 | return None, e 91 | 92 | def extract_array_data(self, response_data, array_path): 93 | # Navigate through the JSON structure to extract the specified array 94 | target_data = response_data 95 | for key in array_path.split('.'): 96 | target_data = target_data.get(key, []) 97 | return target_data if isinstance(target_data, list) else [] 98 | 99 | NODE_CLASS_MAPPINGS = { 100 | "APIRequestNode": APIRequestNode 101 | } 102 | 103 | NODE_DISPLAY_NAME_MAPPINGS = { 104 | "APIRequestNode": "API Request Node" 105 | } 106 | -------------------------------------------------------------------------------- /image_post_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from io import BytesIO 4 | import numpy as np 5 | from PIL import Image 6 | 7 | class PostImageToAPI: 8 | def __init__(self): 9 | pass 10 | 11 | @classmethod 12 | def INPUT_TYPES(cls): 13 | return { 14 | "required": { 15 | "images": ("IMAGE", ), 16 | "api_url": ("STRING", {"default": ""}), 17 | "api_object_id": ("STRING", {"forceInput": True}), 18 | "api_key": ("STRING", {"forceInput": True}) 19 | } 20 | } 21 | 22 | RETURN_TYPES = () 23 | FUNCTION = "post_images" 24 | CATEGORY = "API Manager" 25 | OUTPUT_NODE = True 26 | 27 | def post_images(self, images, api_url, api_object_id, api_key=""): 28 | api_url = api_url.replace("$id", api_object_id) 29 | headers = {'Authorization': api_key} if api_key else {} 30 | results = [] 31 | 32 | for (batch_number, image_tensor) in enumerate(images): 33 | image_np = 255. * image_tensor.cpu().numpy() 34 | image = Image.fromarray(np.clip(image_np, 0, 255).astype(np.uint8)) 35 | buffer = BytesIO() 36 | image.save(buffer, format="PNG") 37 | buffer.seek(0) 38 | 39 | # Specify file name and MIME type 40 | files = {'file': ('image.png', buffer, 'image/png')} 41 | 42 | response = requests.post(api_url, headers=headers, files=files) 43 | 44 | if response.status_code == 200: 45 | results.append(response.json()) 46 | else: 47 | print(f"Error posting image {batch_number}: {response.text}") 48 | 49 | print(f"PostImageToAPI: Posted image to {api_url}\nResponse: {results}") 50 | return {"api_responses": results} 51 | -------------------------------------------------------------------------------- /json_array_iterator.py: -------------------------------------------------------------------------------- 1 | class JSONArrayIteratorNode: 2 | """ 3 | A custom node for iterating through a JSON array and outputting specific objects 4 | based on a selection number provided via the node interface. 5 | """ 6 | 7 | @classmethod 8 | def INPUT_TYPES(cls): 9 | """ 10 | Define input parameters for the JSONArrayIteratorNode. 11 | """ 12 | return { 13 | "required": { 14 | "json_array": ("JSON", {"default": "[]"}), 15 | "selection_number": ("INT", { 16 | "default": 0, 17 | "min": 0, 18 | "max": 9999, # Adjust based on expected maximum array length 19 | "step": 1, 20 | "display": "number" # Allow users to input or select the index number 21 | }), 22 | }, 23 | } 24 | 25 | RETURN_TYPES = ("JSON",) # Output type: the selected JSON object from the array 26 | FUNCTION = "iterate" # The entry-point method for this node 27 | CATEGORY = "Data Processing" # Category under which the node will appear in the UI 28 | 29 | def __init__(self): 30 | pass 31 | 32 | def iterate(self, json_array, selection_number): 33 | print(f"JSONArrayIteratorNode: Iterating with selection_number={selection_number}") 34 | print (f"JSONArrayIteratorNode: Iterating with json_array={json_array}") 35 | 36 | # Check if the selection number is within the bounds of the json_array 37 | if isinstance(json_array, list) and 0 <= selection_number < len(json_array): 38 | selected_object = json_array[selection_number] 39 | else: 40 | print(f"Selection number {selection_number} is out of bounds for the array length {len(json_array)}.") 41 | selected_object = {} # Return an empty object or handle as needed 42 | 43 | return selected_object 44 | -------------------------------------------------------------------------------- /text_prompt_combiner_node.py: -------------------------------------------------------------------------------- 1 | class TextPromptCombinerNode: 2 | def __init__(self): 3 | pass 4 | 5 | @classmethod 6 | def INPUT_TYPES(cls): 7 | return { 8 | "required": { 9 | "text_prompt_template": ("STRING", {"multiline": True}), 10 | "api_response": ("JSON", {"default": {}}), 11 | "id_field_name": ("STRING", {"default": ""}), 12 | "clip": ("CLIP", ) 13 | }, 14 | } 15 | 16 | RETURN_TYPES = ("CONDITIONING", "STRING") # Combined Text Prompt, UUID, Auth Token 17 | RETURN_NAMES = ("CONDITIONING", "ID") 18 | FUNCTION = "execute" 19 | CATEGORY = "API Manager" 20 | 21 | def execute(self, text_prompt_template, api_response, id_field_name, clip): 22 | print("TextPromptCombinerNode: Starting to combine text prompt...") 23 | 24 | # Initialize the combined text prompt with the template 25 | combined_text_prompt = text_prompt_template 26 | 27 | if isinstance(api_response, dict): 28 | # Iterate through each key in the api_response 29 | for key, value in api_response.items(): 30 | # Define the placeholder pattern with the $ prefix 31 | placeholder = f"${key}" 32 | # Replace each occurrence of the placeholder in the template with its corresponding value 33 | combined_text_prompt = combined_text_prompt.replace(placeholder, str(value)) 34 | 35 | # Extract the ID using the id_field_name, if provided and api_response is a dict 36 | extracted_id = api_response.get(id_field_name, "") if isinstance(api_response, dict) else "" 37 | 38 | print(f"Combined Text Prompt: {combined_text_prompt}") 39 | print(f"Extracted ID: {extracted_id}") 40 | 41 | # Now encode the combined text using CLIP, similar to CLIPTextEncode class 42 | tokens = clip.tokenize(combined_text_prompt) 43 | cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) 44 | 45 | # Return the conditioning and the extracted ID 46 | return ([[cond, {"pooled_output": pooled}]], extracted_id) 47 | --------------------------------------------------------------------------------