From 5e685905d028836500cdedc9fb325225f67a4bc6 Mon Sep 17 00:00:00 2001 From: CC-BryanOttho Date: Wed, 28 Feb 2024 00:31:39 +0100 Subject: [PATCH] initial commit --- .gitignore | 24 ++++++++ README.md | 63 +++++++++++++++++++++ __init__.py | 20 +++++++ api_request.py | 105 +++++++++++++++++++++++++++++++++++ image_post_node.py | 50 +++++++++++++++++ json_array_iterator.py | 43 ++++++++++++++ text_prompt_combiner_node.py | 46 +++++++++++++++ 7 files changed, 351 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 api_request.py create mode 100644 image_post_node.py create mode 100644 json_array_iterator.py create mode 100644 text_prompt_combiner_node.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb08510 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# Virtual Environment +venv/ +env/ +ENV/ + +# IDE +.vscode/ + +# Compiled files +*.pyc + +# Logs and databases +*.log +*.sqlite3 + +# Dependency directories +lib/ +lib64/ diff --git a/README.md b/README.md index 1f89a67..823e3d1 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ # ComfyUI_API_Manager +``` +# Custom Workflow Nodes for API Integration + +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. + +## Installation + +To use these custom nodes, clone this repository into your `custom_nodes` directory. No external dependencies are required. + +```bash +git clone https://github.com/CC-BryanOttho/ComfyUI_API_Manager.git custom_nodes +``` + +## Usage + +### API Request Node + +This node performs API requests and processes the responses: + +- **auth_url**: Specify the authentication endpoint of the API. +- **auth_body_text**: Enter the payload for the API authentication request. +- **array_path**: Navigate through the response object to locate the desired object or array, especially useful if it's nested. + +1. Configure the `auth_url` and `auth_body_text` to authenticate with your target API. +2. Use the `array_path` to specify the path to the data of interest in the API response. + +### Text Prompt Combiner Node + +Utilizes API response data for dynamic text manipulation: + +- Dynamically replace text in a template using `$attributeName` syntax to insert values from the API response. +- **id_field_name**: Specify the attribute name whose value is returned as a string, aiding in identifying or referencing objects. + +1. Insert API response data into a text box, using `$attribute1.attribute2` to reference nested attributes. +2. The `id_field_name` can be used to extract specific values from the response for further use. + +### Image Post Node + +Facilitates the posting of images to an API: + +- Requires an image, an object ID as a string, and an API key for authentication. +- **api_url**: The endpoint for image posting, with `$id` used in the URL to dynamically insert the ID. + +1. Provide the necessary image, API object ID, and API key. +2. Configure the `api_url`, ensuring to include `$id` where the object ID should be inserted in the URL. + +## Example Workflow + +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: + +1. **API Request Node** fetches the desired data. +2. **Text Prompt Combiner Node** generates a dynamic text prompt based on the fetched data. +3. **Image Post Node** posts an image using details from the previous nodes. + +## Additional Information + +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. + +## Contributing + +Contributions are welcome! If you have improvements or bug fixes, please submit a pull request or open an issue. + +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..70b7b52 --- /dev/null +++ b/__init__.py @@ -0,0 +1,20 @@ +from .api_request import APIRequestNode +from .image_post_node import PostImageToAPI +from .text_prompt_combiner_node import TextPromptCombinerNode + +# Define the mappings for your custom nodes +NODE_CLASS_MAPPINGS = { + "APIRequestNode": APIRequestNode, + "PostImageToAPI": PostImageToAPI, + "TextPromptCombinerNode": TextPromptCombinerNode, +} + +# Define human-readable names for your nodes (optional) +NODE_DISPLAY_NAME_MAPPINGS = { + "APIRequestNode": "API Request Node", + "PostImageToAPI": "Image Post Node", + "TextPromptCombinerNode": "Text Prompt Combiner Node", +} + +# Export the mappings for use by ComfyUI +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/api_request.py b/api_request.py new file mode 100644 index 0000000..c1b9397 --- /dev/null +++ b/api_request.py @@ -0,0 +1,105 @@ +import datetime +import requests +import json + +class APIRequestNode: + """ + A custom node for making API requests and processing responses. + + Attributes and class methods are defined to match the framework's expectations, + similar to the provided Example node structure. + """ + @classmethod + def INPUT_TYPES(cls): + """ + Define input parameters for the APIRequestNode. + """ + return { + "required": { + "api_url": ("STRING", {"default": ""}), + "auth_url": ("STRING", {"default": ""}), + "token_attribute_name": ("STRING", {"default": ""}), + "auth_body_text": ("STRING", {"multiline": True}), + "array_path": ("STRING", {"default": ""}), + "iteration_index": ("INT", { + "default": 0, + "min": 0, + "max": 9999, # Adjust based on expected maximum array length + "step": 1, + "display": "number" # Allow users to input or select the index number + }) + } + } + + RETURN_TYPES = ("JSON", "INT", "STRING") # Output types: JSON response + RETURN_NAMES = ("API_RESPONSE", "LENGTH", "API_KEY") + FUNCTION = "execute" # The entry-point method for this node + CATEGORY = "API Manager" # Category under which the node will appear in the UI + + def __init__(self): + pass + + def authenticate(self, auth_url, auth_body, token_attribute_name): + """ + Authenticate and retrieve a bearer token using the provided authentication URL and body. + """ + try: + response = requests.post(auth_url, json=auth_body) + response.raise_for_status() + token = response.json().get(token_attribute_name) + print(f"APIRequestNode: Obtained token") + return token + except requests.exceptions.RequestException as e: + print(f"APIRequestNode: Error obtaining bearer token - {e}") + return None + + def execute(self, api_url, auth_url, token_attribute_name, auth_body_text, array_path, iteration_index): + """ + The main logic for making an API request, processing the response, + and potentially queuing image generation requests or other post-processing steps. + """ + auth_body = None + if auth_body_text: + try: + auth_body = json.loads("{" + auth_body_text + "}") + except json.JSONDecodeError as e: + print(f"Error parsing JSON input: {e}") + return None, None, None + + token = self.authenticate(auth_url, auth_body, token_attribute_name) if auth_url else None + headers = {'Authorization': f'Bearer {token}'} if token else {} + response_data, error = self.make_api_request(api_url, headers) + + if error: + return {}, None, "" + + array_data = self.extract_array_data(response_data, array_path) if array_path else response_data + selected_item = array_data[iteration_index] if isinstance(array_data, list) else array_data + return selected_item, len(array_data), f'Bearer {token}' + + def make_api_request(self, api_url, headers, params={}): + """ + Make the actual API request and return the response. + """ + try: + response = requests.get(api_url, headers=headers, params=params) + response.raise_for_status() + return response.json(), None + except requests.exceptions.RequestException as e: + print(f"Error making API request: {e}") + return None, e + + def extract_array_data(self, response_data, array_path): + # Navigate through the JSON structure to extract the specified array + target_data = response_data + for key in array_path.split('.'): + target_data = target_data.get(key, []) + return target_data if isinstance(target_data, list) else [] + +NODE_CLASS_MAPPINGS = { + "APIRequestNode": APIRequestNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "APIRequestNode": "API Request Node" +} diff --git a/image_post_node.py b/image_post_node.py new file mode 100644 index 0000000..6d5e147 --- /dev/null +++ b/image_post_node.py @@ -0,0 +1,50 @@ +import json +import requests +from io import BytesIO +import numpy as np +from PIL import Image + +class PostImageToAPI: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", ), + "api_url": ("STRING", {"default": ""}), + "api_object_id": ("STRING", {"forceInput": True}), + "api_key": ("STRING", {"forceInput": True}) + } + } + + RETURN_TYPES = () + FUNCTION = "post_images" + CATEGORY = "API Manager" + OUTPUT_NODE = True + + def post_images(self, images, api_url, api_object_id, api_key=""): + api_url = api_url.replace("$id", api_object_id) + headers = {'Authorization': api_key} if api_key else {} + results = [] + + for (batch_number, image_tensor) in enumerate(images): + image_np = 255. * image_tensor.cpu().numpy() + image = Image.fromarray(np.clip(image_np, 0, 255).astype(np.uint8)) + buffer = BytesIO() + image.save(buffer, format="PNG") + buffer.seek(0) + + # Specify file name and MIME type + files = {'file': ('image.png', buffer, 'image/png')} + + response = requests.post(api_url, headers=headers, files=files) + + if response.status_code == 200: + results.append(response.json()) + else: + print(f"Error posting image {batch_number}: {response.text}") + + print(f"PostImageToAPI: Posted image to {api_url}\nResponse: {results}") + return {"api_responses": results} diff --git a/json_array_iterator.py b/json_array_iterator.py new file mode 100644 index 0000000..a2c9e3b --- /dev/null +++ b/json_array_iterator.py @@ -0,0 +1,43 @@ +class JSONArrayIteratorNode: + """ + A custom node for iterating through a JSON array and outputting specific objects + based on a selection number provided via the node interface. + """ + + @classmethod + def INPUT_TYPES(cls): + """ + Define input parameters for the JSONArrayIteratorNode. + """ + return { + "required": { + "json_array": ("JSON", {"default": "[]"}), + "selection_number": ("INT", { + "default": 0, + "min": 0, + "max": 9999, # Adjust based on expected maximum array length + "step": 1, + "display": "number" # Allow users to input or select the index number + }), + }, + } + + RETURN_TYPES = ("JSON",) # Output type: the selected JSON object from the array + FUNCTION = "iterate" # The entry-point method for this node + CATEGORY = "Data Processing" # Category under which the node will appear in the UI + + def __init__(self): + pass + + def iterate(self, json_array, selection_number): + print(f"JSONArrayIteratorNode: Iterating with selection_number={selection_number}") + print (f"JSONArrayIteratorNode: Iterating with json_array={json_array}") + + # Check if the selection number is within the bounds of the json_array + if isinstance(json_array, list) and 0 <= selection_number < len(json_array): + selected_object = json_array[selection_number] + else: + print(f"Selection number {selection_number} is out of bounds for the array length {len(json_array)}.") + selected_object = {} # Return an empty object or handle as needed + + return selected_object diff --git a/text_prompt_combiner_node.py b/text_prompt_combiner_node.py new file mode 100644 index 0000000..6c01c7e --- /dev/null +++ b/text_prompt_combiner_node.py @@ -0,0 +1,46 @@ +class TextPromptCombinerNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_prompt_template": ("STRING", {"multiline": True}), + "api_response": ("JSON", {"default": {}}), + "id_field_name": ("STRING", {"default": ""}), + "clip": ("CLIP", ) + }, + } + + RETURN_TYPES = ("CONDITIONING", "STRING") # Combined Text Prompt, UUID, Auth Token + RETURN_NAMES = ("CONDITIONING", "ID") + FUNCTION = "execute" + CATEGORY = "API Manager" + + def execute(self, text_prompt_template, api_response, id_field_name, clip): + print("TextPromptCombinerNode: Starting to combine text prompt...") + + # Initialize the combined text prompt with the template + combined_text_prompt = text_prompt_template + + if isinstance(api_response, dict): + # Iterate through each key in the api_response + for key, value in api_response.items(): + # Define the placeholder pattern with the $ prefix + placeholder = f"${key}" + # Replace each occurrence of the placeholder in the template with its corresponding value + combined_text_prompt = combined_text_prompt.replace(placeholder, str(value)) + + # Extract the ID using the id_field_name, if provided and api_response is a dict + extracted_id = api_response.get(id_field_name, "") if isinstance(api_response, dict) else "" + + print(f"Combined Text Prompt: {combined_text_prompt}") + print(f"Extracted ID: {extracted_id}") + + # Now encode the combined text using CLIP, similar to CLIPTextEncode class + tokens = clip.tokenize(combined_text_prompt) + cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) + + # Return the conditioning and the extracted ID + return ([[cond, {"pooled_output": pooled}]], extracted_id)