commit 70efdb04ed40cb649a8e35494606eacfefb46acf Author: lum3on Date: Tue Sep 16 14:37:52 2025 +0200 Initial release of ComfyUI Reve API Integration Node - Unified node supporting Create, Edit, and Remix operations - Dynamic input system based on operation type - Comprehensive error handling and credit tracking - Verbose logging for debugging and monitoring - Clean public release with documentation and examples diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eda01cd --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Reve API Configuration +# Copy this file to .env and add your actual API key + +# Your Reve API key (required) +REVE_API_KEY=your_reve_api_key_here + +# Optional: API timeout in seconds (default: 60) +# REVE_API_TIMEOUT=60 + +# Optional: Enable debug logging (default: false) +# REVE_DEBUG=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5a5526 --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Development and test files +test_*.py +debug_*.py +check_*.py +monitor_*.py +*_test.py +*_debug.py + +# Temporary files +*.tmp +*.temp +*.bak +*.backup + +# API keys and secrets +.env +*.key +*.secret +api_keys.txt + +# ComfyUI specific +output/ +temp/ +models/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfdfd50 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# ComfyUI Reve API Integration Node + +A unified ComfyUI custom node that integrates all Reve API endpoints (Create, Edit, Remix) into a single, dynamic node with operation-specific inputs and seamless operation switching. + +## ✅ Current Status + +**The Reve API is now working!** This integration has been updated with the correct API endpoints and is fully functional. + +The API uses the endpoints `/v1/image/create`, `/v1/image/edit`, and `/v1/image/remix`. Authentication works with API keys in the format `papi.xxxxx`. + +## Features + +- **Unified Interface**: Single node handles all three operations (Create, Edit, Remix) +- **Dynamic Inputs**: Show/hide parameters based on selected operation type +- **Consistent Outputs**: Standardized output format across all operations +- **Error Handling**: Comprehensive error management with operation-specific context +- **Credit Tracking**: Integrated credit monitoring and usage tracking +- **Verbose Logging**: Optional detailed logging for debugging and monitoring API interactions + +## Operations + +### Create +Generate images from text prompts. +- **Cost**: 18 credits per generation +- **Inputs**: prompt, aspect_ratio, version, output_format + +### Edit +Modify existing images with text instructions. +- **Cost**: 30 credits per edit +- **Inputs**: prompt, reference_image, version, output_format + +### Remix +Combine multiple images with text guidance. +- **Cost**: 30 credits per remix +- **Inputs**: prompt, reference_images (1-4), aspect_ratio, version, output_format + +## Installation + +1. Clone or download this repository to your ComfyUI custom_nodes directory: + ```bash + cd ComfyUI/custom_nodes + git clone ComfyUI_Reve-API + ``` + +2. Install dependencies: + ```bash + cd ComfyUI_Reve-API + pip install -r requirements.txt + ``` + +3. Restart ComfyUI + +## Usage + +1. Add the "Reve API (Create/Edit/Remix)" node to your workflow +2. Enter your Reve API key in the api_key field +3. Select the desired operation from the dropdown +4. Configure parameters based on the operation: + - **Create**: Enter prompt and select aspect ratio + - **Edit**: Connect reference image and enter editing instructions + - **Remix**: Connect reference images (1-4) and enter combination prompt +5. Execute the workflow + +## Outputs + +The node provides five outputs: +- **image**: Generated/edited image tensor +- **metadata**: JSON string with operation details +- **request_id**: Unique identifier for the API request +- **credits_used**: Number of credits consumed +- **credits_remaining**: Remaining credit balance + +## API Key Management + +Your Reve API key is entered directly in the node's api_key field. The key is required for all operations and should be in the format `papi.xxxxx`. + +## Verbose Logging + +The node includes an optional verbose logging feature that provides detailed information about API interactions, performance metrics, and debugging information. + +### Enabling Verbose Logging + +1. In the node interface, set the `verbose_logging` parameter to `True` +2. Check the ComfyUI console for detailed log output + +### What Gets Logged + +When verbose logging is enabled, the following information is logged to the console: + +- **Operation Start**: Details about the operation being performed +- **Request Parameters**: Information about the API request (without sensitive data) +- **API Response**: Request ID, credits used/remaining, processing time +- **Performance Metrics**: Total processing time, API call time, image processing time +- **Error Details**: Detailed error information when operations fail + +### Example Verbose Output + +``` +2025-09-16 12:38:26,317 - nodes.reve_api_node.verbose - INFO - 🚀 Starting Reve API CREATE operation +{ + "prompt_length": 21, + "operation": "create", + "version": "latest", + "aspect_ratio": "16:9", + "output_format": "png" +} + +2025-09-16 12:38:28,842 - nodes.reve_api_node.verbose - INFO - 🔍 Reve API CREATE Operation Complete +{ + "operation": "create", + "request_id": "req-abc123", + "credits_used": 18, + "credits_remaining": 482, + "processing_time_seconds": 2.5, + "success": true +} + +2025-09-16 12:38:28,845 - nodes.reve_api_node.verbose - INFO - ✅ Reve API CREATE operation completed successfully +{ + "total_processing_time_seconds": 2.528, + "api_call_time_seconds": 2.5, + "image_processing_time_seconds": 0.028 +} +``` + +### Benefits of Verbose Logging + +- **Debugging**: Identify issues with API calls or image processing +- **Performance Monitoring**: Track processing times and optimize workflows +- **Credit Tracking**: Monitor credit usage across operations +- **Audit Trail**: Keep detailed records of API interactions +- **Troubleshooting**: Get detailed error information for support + +### Note on Performance + +Verbose logging has minimal performance impact when disabled (default). When enabled, it adds negligible overhead for logging operations. + +## Error Handling + +The node provides detailed error messages for common issues: +- Invalid API key +- Insufficient credits +- Content policy violations +- Rate limiting +- Network errors + +## Requirements + +- ComfyUI +- Python 3.8+ +- PyTorch (included with ComfyUI) +- Pillow (included with ComfyUI) +- requests +- numpy (included with ComfyUI) + +## Troubleshooting + +### Node doesn't appear in ComfyUI +- Check that all dependencies are installed +- Verify the node is in the correct custom_nodes directory +- Check ComfyUI console for error messages + +### API errors +- Verify your API key is correct +- Check your credit balance +- Ensure images meet size requirements (max 10MB for edit, 5MB each for remix) + +### Image conversion errors +- Ensure input images are valid ComfyUI image tensors +- Check image dimensions are reasonable (512x512 to 2048x2048 recommended) + +## Support + +For issues related to: +- **Node functionality**: Check the GitHub repository issues +- **Reve API**: Contact Reve API support +- **ComfyUI integration**: Check ComfyUI documentation + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## Changelog + +### v1.2.0 +- **CHANGED**: API key is now required in the node interface - no more environment variable setup needed +- **REMOVED**: Environment variable fallback for API key (REVE_API_KEY) +- **IMPROVED**: Simplified setup process - users just paste their API key directly in the node +- **ENHANCED**: Better API key validation with clear error messages + +### v1.1.1 +- **FIXED**: HTTP 400 error in edit operation by using correct `edit_instruction` parameter +- **REMOVED**: Unsupported `aspect_ratio` parameter from edit operation +- **IMPROVED**: Error handling to show detailed API error messages instead of generic HTTP codes +- **ENHANCED**: Debugging capabilities with better error message extraction + +### v1.1.0 +- **NEW**: Verbose logging functionality for debugging and monitoring +- **CHANGED**: Simplified node outputs to only return image and metadata +- **ENHANCED**: Credit tracking and performance metrics now available via verbose logging +- **IMPROVED**: Better structured metadata output with JSON formatting + +### v1.0.0 +- Initial release +- Unified node supporting Create, Edit, and Remix operations +- Dynamic input system +- Comprehensive error handling +- Credit tracking and monitoring diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a552948 --- /dev/null +++ b/__init__.py @@ -0,0 +1,48 @@ +""" +ComfyUI Reve API Integration Node + +This package provides a unified ComfyUI custom node for integrating with the Reve API, +supporting Create, Edit, and Remix operations in a single dynamic node. +""" + +import os +import sys + +# Add current directory to path for imports +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, current_dir) + +try: + from .nodes.reve_api_node import ReveAPI + + # Node class mappings for ComfyUI + NODE_CLASS_MAPPINGS = { + "ReveAPI": ReveAPI + } + + # Display name mappings for ComfyUI interface + NODE_DISPLAY_NAME_MAPPINGS = { + "ReveAPI": "Reve API (Create/Edit/Remix)" + } + + # Web directory for any custom UI components (if needed in future) + WEB_DIRECTORY = "./web" + + print("✅ ComfyUI Reve API Integration loaded successfully") + +except ImportError as e: + print(f"❌ Failed to load ComfyUI Reve API Integration: {e}") + print("Please ensure all dependencies are installed:") + print(" pip install requests pillow") + + # Create empty mappings to prevent ComfyUI errors + NODE_CLASS_MAPPINGS = {} + NODE_DISPLAY_NAME_MAPPINGS = {} + +except Exception as e: + print(f"❌ Unexpected error loading ComfyUI Reve API Integration: {e}") + NODE_CLASS_MAPPINGS = {} + NODE_DISPLAY_NAME_MAPPINGS = {} + +# Export for ComfyUI +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..2d7fc1c --- /dev/null +++ b/core/__init__.py @@ -0,0 +1 @@ +# ComfyUI Reve API Integration - Core Module diff --git a/core/image_utils.py b/core/image_utils.py new file mode 100644 index 0000000..6fa7e82 --- /dev/null +++ b/core/image_utils.py @@ -0,0 +1,336 @@ +""" +Image Utilities Module + +This module provides utilities for converting between ComfyUI image tensors +and base64 encoded images required by the Reve API, with support for different +formats, size validation, and quality preservation. +""" + +import io +import base64 +import logging +from typing import List, Tuple, Optional, Union +from PIL import Image, ImageOps +import numpy as np + +try: + import torch +except ImportError: + raise ImportError("PyTorch is required. This should be available in ComfyUI environment.") + +# Configure logging +logger = logging.getLogger(__name__) + + +class ImageConversionError(Exception): + """Exception raised for image conversion errors""" + pass + + +def tensor_to_pil(tensor: torch.Tensor) -> Image.Image: + """ + Convert ComfyUI image tensor to PIL Image. + + Args: + tensor: ComfyUI image tensor (B, H, W, C) with values in [0, 1] + + Returns: + PIL Image object + + Raises: + ImageConversionError: If tensor format is invalid + """ + try: + # Handle batch dimension - take first image if batch + if tensor.dim() == 4: + tensor = tensor[0] + elif tensor.dim() != 3: + raise ImageConversionError(f"Expected 3D or 4D tensor, got {tensor.dim()}D") + + # Ensure tensor is in correct format (H, W, C) + if tensor.shape[-1] not in [1, 3, 4]: + raise ImageConversionError(f"Expected 1, 3, or 4 channels, got {tensor.shape[-1]}") + + # Convert to numpy and scale to [0, 255] + numpy_image = tensor.cpu().numpy() + numpy_image = np.clip(numpy_image * 255.0, 0, 255).astype(np.uint8) + + # Convert to PIL Image + if numpy_image.shape[-1] == 1: + # Grayscale + pil_image = Image.fromarray(numpy_image.squeeze(-1), mode='L') + elif numpy_image.shape[-1] == 3: + # RGB + pil_image = Image.fromarray(numpy_image, mode='RGB') + elif numpy_image.shape[-1] == 4: + # RGBA + pil_image = Image.fromarray(numpy_image, mode='RGBA') + else: + raise ImageConversionError(f"Unsupported channel count: {numpy_image.shape[-1]}") + + return pil_image + + except Exception as e: + raise ImageConversionError(f"Failed to convert tensor to PIL: {str(e)}") + + +def pil_to_tensor(pil_image: Image.Image) -> torch.Tensor: + """ + Convert PIL Image to ComfyUI image tensor. + + Args: + pil_image: PIL Image object + + Returns: + ComfyUI image tensor (1, H, W, C) with values in [0, 1] + + Raises: + ImageConversionError: If image format is invalid + """ + try: + # Convert to RGB if necessary + if pil_image.mode not in ['RGB', 'RGBA', 'L']: + pil_image = pil_image.convert('RGB') + + # Convert to numpy array + numpy_image = np.array(pil_image) + + # Handle grayscale + if len(numpy_image.shape) == 2: + numpy_image = numpy_image[:, :, np.newaxis] + + # Normalize to [0, 1] and convert to float32 + numpy_image = numpy_image.astype(np.float32) / 255.0 + + # Convert to tensor and add batch dimension + tensor = torch.from_numpy(numpy_image).unsqueeze(0) + + return tensor + + except Exception as e: + raise ImageConversionError(f"Failed to convert PIL to tensor: {str(e)}") + + +def tensor_to_base64(tensor: torch.Tensor, format: str = "PNG", quality: int = 95) -> str: + """ + Convert ComfyUI image tensor to base64 encoded string. + + Args: + tensor: ComfyUI image tensor + format: Output format (PNG, JPEG, WEBP) + quality: JPEG/WEBP quality (1-100) + + Returns: + Base64 encoded image string + + Raises: + ImageConversionError: If conversion fails + """ + try: + # Convert tensor to PIL + pil_image = tensor_to_pil(tensor) + + # Convert to RGB for JPEG (no transparency support) + if format.upper() == "JPEG" and pil_image.mode in ['RGBA', 'LA']: + # Create white background + background = Image.new('RGB', pil_image.size, (255, 255, 255)) + if pil_image.mode == 'RGBA': + background.paste(pil_image, mask=pil_image.split()[-1]) + else: + background.paste(pil_image) + pil_image = background + + # Save to bytes buffer + buffer = io.BytesIO() + save_kwargs = {"format": format.upper()} + + if format.upper() in ["JPEG", "WEBP"]: + save_kwargs["quality"] = quality + save_kwargs["optimize"] = True + + pil_image.save(buffer, **save_kwargs) + + # Encode to base64 + buffer.seek(0) + base64_string = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return base64_string + + except Exception as e: + raise ImageConversionError(f"Failed to convert tensor to base64: {str(e)}") + + +def base64_to_tensor(base64_string: str) -> torch.Tensor: + """ + Convert base64 encoded image string to ComfyUI image tensor. + + Args: + base64_string: Base64 encoded image + + Returns: + ComfyUI image tensor + + Raises: + ImageConversionError: If conversion fails + """ + try: + # Decode base64 + image_bytes = base64.b64decode(base64_string) + + # Load as PIL Image + buffer = io.BytesIO(image_bytes) + pil_image = Image.open(buffer) + + # Convert to tensor + tensor = pil_to_tensor(pil_image) + + return tensor + + except Exception as e: + raise ImageConversionError(f"Failed to convert base64 to tensor: {str(e)}") + + +def validate_image_size(tensor: torch.Tensor, max_size_mb: float = 10.0) -> bool: + """ + Validate image tensor size for API upload. + + Args: + tensor: ComfyUI image tensor + max_size_mb: Maximum size in megabytes + + Returns: + True if size is acceptable, False otherwise + """ + try: + # Convert to base64 to get actual upload size + base64_string = tensor_to_base64(tensor, format="PNG") + size_bytes = len(base64_string.encode('utf-8')) + size_mb = size_bytes / (1024 * 1024) + + return size_mb <= max_size_mb + + except Exception: + return False + + +def resize_image_if_needed(tensor: torch.Tensor, max_dimension: int = 2048) -> torch.Tensor: + """ + Resize image tensor if it exceeds maximum dimensions. + + Args: + tensor: ComfyUI image tensor + max_dimension: Maximum width or height + + Returns: + Resized tensor if needed, original tensor otherwise + """ + try: + # Get dimensions (handle batch dimension) + if tensor.dim() == 4: + _, height, width, _ = tensor.shape + else: + height, width, _ = tensor.shape + + # Check if resize is needed + if height <= max_dimension and width <= max_dimension: + return tensor + + # Calculate new dimensions maintaining aspect ratio + if height > width: + new_height = max_dimension + new_width = int(width * (max_dimension / height)) + else: + new_width = max_dimension + new_height = int(height * (max_dimension / width)) + + # Convert to PIL, resize, and convert back + pil_image = tensor_to_pil(tensor) + pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS) + resized_tensor = pil_to_tensor(pil_image) + + logger.info(f"Resized image from {width}x{height} to {new_width}x{new_height}") + return resized_tensor + + except Exception as e: + logger.warning(f"Failed to resize image: {str(e)}") + return tensor + + +def prepare_image_for_api(tensor: torch.Tensor, format: str = "PNG", + max_size_mb: float = 10.0, max_dimension: int = 2048) -> str: + """ + Prepare ComfyUI image tensor for Reve API upload. + + Handles resizing, format conversion, and size validation. + + Args: + tensor: ComfyUI image tensor + format: Output format (PNG, JPEG, WEBP) + max_size_mb: Maximum file size in MB + max_dimension: Maximum width/height + + Returns: + Base64 encoded image string ready for API + + Raises: + ImageConversionError: If preparation fails + """ + try: + # Resize if needed + processed_tensor = resize_image_if_needed(tensor, max_dimension) + + # Convert to base64 + base64_string = tensor_to_base64(processed_tensor, format) + + # Validate size + size_bytes = len(base64_string.encode('utf-8')) + size_mb = size_bytes / (1024 * 1024) + + if size_mb > max_size_mb: + # Try JPEG with lower quality if PNG is too large + if format.upper() == "PNG": + logger.warning(f"PNG too large ({size_mb:.1f}MB), trying JPEG") + base64_string = tensor_to_base64(processed_tensor, "JPEG", quality=85) + size_bytes = len(base64_string.encode('utf-8')) + size_mb = size_bytes / (1024 * 1024) + + if size_mb > max_size_mb: + raise ImageConversionError(f"Image too large even after compression: {size_mb:.1f}MB > {max_size_mb}MB") + + logger.debug(f"Prepared image: {size_mb:.1f}MB, format: {format}") + return base64_string + + except Exception as e: + raise ImageConversionError(f"Failed to prepare image for API: {str(e)}") + + +def prepare_multiple_images_for_api(tensors: List[torch.Tensor], format: str = "PNG", + max_size_mb_each: float = 5.0) -> List[str]: + """ + Prepare multiple ComfyUI image tensors for Reve API (Remix operation). + + Args: + tensors: List of ComfyUI image tensors + format: Output format + max_size_mb_each: Maximum size per image in MB + + Returns: + List of base64 encoded image strings + + Raises: + ImageConversionError: If preparation fails + """ + if not tensors or len(tensors) > 4: + raise ImageConversionError("Must provide 1-4 images for remix operation") + + base64_images = [] + for i, tensor in enumerate(tensors): + try: + base64_string = prepare_image_for_api( + tensor, format, max_size_mb_each, max_dimension=1536 # Smaller for multiple images + ) + base64_images.append(base64_string) + except Exception as e: + raise ImageConversionError(f"Failed to prepare image {i+1}: {str(e)}") + + return base64_images diff --git a/core/reve_api_client.py b/core/reve_api_client.py new file mode 100644 index 0000000..c3d8945 --- /dev/null +++ b/core/reve_api_client.py @@ -0,0 +1,425 @@ +""" +Reve API Client Module + +This module provides a comprehensive HTTP client for interacting with the Reve API, +supporting Create, Edit, and Remix operations with proper authentication, error handling, +and retry logic. +""" + +import os +import json +import time +import base64 +import logging +from typing import Dict, Any, Optional, Union, List +from dataclasses import dataclass +from enum import Enum + +try: + import requests + from requests.adapters import HTTPAdapter + from urllib3.util.retry import Retry +except ImportError: + raise ImportError("requests library is required. Install with: pip install requests") + + +# Configure logging +logger = logging.getLogger(__name__) + + + + + +class ReveOperation(Enum): + """Supported Reve API operations""" + CREATE = "create" + EDIT = "edit" + REMIX = "remix" + + +class ReveError(Exception): + """Base exception for Reve API errors""" + def __init__(self, message: str, error_code: str = None, request_id: str = None): + super().__init__(message) + self.error_code = error_code + self.request_id = request_id + + +class ReveAuthenticationError(ReveError): + """Authentication related errors""" + pass + + +class ReveContentPolicyError(ReveError): + """Content policy violation errors""" + pass + + +class ReveRateLimitError(ReveError): + """Rate limiting errors""" + def __init__(self, message: str, retry_after: int = None, **kwargs): + super().__init__(message, **kwargs) + self.retry_after = retry_after + + +@dataclass +class ReveResponse: + """Standardized response from Reve API""" + success: bool + image: Optional[str] = None # Base64 encoded image + metadata: Optional[Dict[str, Any]] = None + request_id: Optional[str] = None + credits_used: Optional[int] = None + credits_remaining: Optional[int] = None + error_code: Optional[str] = None + error_message: Optional[str] = None + + +class ReveAPIClient: + """ + Core HTTP client for Reve API operations. + + Handles authentication, request formatting, response parsing, + error handling, and retry logic for all Reve API endpoints. + """ + + BASE_URL = "https://api.reve.com" + DEFAULT_TIMEOUT = 60 + MAX_RETRIES = 3 + BACKOFF_FACTOR = 1.0 + + def __init__(self, api_key: str, timeout: int = DEFAULT_TIMEOUT): + """ + Initialize the Reve API client. + + Args: + api_key: Reve API key (required) + timeout: Request timeout in seconds + """ + if not api_key or api_key.strip() == "": + raise ReveAuthenticationError("API key is required.") + + self.api_key = api_key.strip() + self.timeout = timeout + self.session = self._create_session() + + def _create_session(self) -> requests.Session: + """Create a requests session with retry strategy and authentication.""" + session = requests.Session() + + # Configure retry strategy + retry_strategy = Retry( + total=self.MAX_RETRIES, + backoff_factor=self.BACKOFF_FACTOR, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["POST"] + ) + + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Set default headers + session.headers.update({ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "User-Agent": "ComfyUI-Reve-API/1.0" + }) + + return session + + def _make_request(self, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]: + """ + Make HTTP request to Reve API endpoint. + + Args: + endpoint: API endpoint path + payload: Request payload + + Returns: + Parsed JSON response + + Raises: + ReveError: For various API errors + """ + url = f"{self.BASE_URL}{endpoint}" + + try: + logger.debug(f"Making request to {url}") + response = self.session.post(url, json=payload, timeout=self.timeout) + + # Handle rate limiting with custom retry + if response.status_code == 429: + retry_after = int(response.headers.get("Retry-After", 60)) + raise ReveRateLimitError( + f"Rate limit exceeded. Retry after {retry_after} seconds.", + retry_after=retry_after, + request_id=response.headers.get("X-Request-ID") + ) + + # Check if we got HTML instead of JSON (API not available) + content_type = response.headers.get("content-type", "").lower() + if "text/html" in content_type: + raise ReveError( + "Reve API endpoints are not available. The API may be in development or requires special access. " + "Please check https://api.reve.com for the latest API status or contact Reve support.", + "API_NOT_AVAILABLE", + None + ) + + # Parse response + try: + response_data = response.json() + except json.JSONDecodeError: + # Special handling for 404 errors that return HTML + if response.status_code == 404: + raise ReveError( + "Reve API endpoint not found. The API endpoints may have changed or the API is not yet publicly available. " + "Please check https://api.reve.com for the latest API documentation.", + "ENDPOINT_NOT_FOUND", + None + ) + response_data = {"error": {"message": "Invalid JSON response"}} + + # Handle HTTP errors + if not response.ok: + # Try to get detailed error information from response + error_info = response_data.get("error", {}) + + # If no error object, check for direct error fields + if not error_info: + error_info = { + "code": response_data.get("code"), + "message": response_data.get("message") or response_data.get("error") + } + + error_code = error_info.get("code", f"HTTP_{response.status_code}") + error_message = error_info.get("message", f"HTTP {response.status_code} error") + request_id = response_data.get("request_id") or response.headers.get("X-Request-ID") + + # Add raw response data for debugging if error message is generic + if error_message == f"HTTP {response.status_code} error": + logger.debug(f"Raw response data: {response_data}") + # Try to extract any error information from the response + if isinstance(response_data, dict): + # Look for common error field names + for field in ["detail", "details", "msg", "description"]: + if field in response_data: + error_message = f"HTTP {response.status_code}: {response_data[field]}" + break + else: + # If we have response data but no clear error message, include it + if response_data: + error_message = f"HTTP {response.status_code}: {json.dumps(response_data)}" + + if response.status_code == 401: + raise ReveAuthenticationError(error_message, error_code, request_id) + elif response.status_code == 404: + raise ReveError( + "Reve API endpoint not found. The API endpoints may have changed or the API is not yet publicly available. " + "Please check https://api.reve.com for the latest API documentation.", + "ENDPOINT_NOT_FOUND", + request_id + ) + elif error_code == "CONTENT_POLICY_VIOLATION": + raise ReveContentPolicyError(error_message, error_code, request_id) + else: + raise ReveError(error_message, error_code, request_id) + + return response_data + + except requests.exceptions.Timeout: + raise ReveError(f"Request timeout after {self.timeout} seconds") + except requests.exceptions.ConnectionError: + raise ReveError("Connection error. Please check your internet connection.") + except requests.exceptions.RequestException as e: + raise ReveError(f"Request failed: {str(e)}") + + def create(self, prompt: str, aspect_ratio: str = "3:2", version: str = "latest") -> ReveResponse: + """ + Create image from text prompt. + + Args: + prompt: Text description of desired image + aspect_ratio: Image aspect ratio (16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 1:1) + version: Model version (latest, reve-create@20250915) + + Returns: + ReveResponse with generated image and metadata (always returns PNG format) + """ + payload = { + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "version": version + } + + try: + response_data = self._make_request("/v1/image/create", payload) + + # Create metadata from response fields + metadata = { + "version": response_data.get("version"), + "content_violation": response_data.get("content_violation"), + "aspect_ratio": aspect_ratio, + "prompt": prompt + } + + return ReveResponse( + success=True, + image=response_data.get("image"), + metadata=metadata, + request_id=response_data.get("request_id"), + credits_used=response_data.get("credits_used"), + credits_remaining=response_data.get("credits_remaining") + ) + except ReveError as e: + return ReveResponse( + success=False, + error_code=e.error_code, + error_message=str(e), + request_id=e.request_id + ) + + def edit(self, prompt: str, reference_image: str, version: str = "latest") -> ReveResponse: + """ + Edit existing image with text instructions. + + Args: + prompt: Text instruction for editing (sent as edit_instruction to API) + reference_image: Base64 encoded source image + version: Model version (latest, reve-edit@20250915) + + + Returns: + ReveResponse with edited image and metadata + """ + payload = { + "edit_instruction": prompt, # API expects 'edit_instruction', not 'prompt' + "reference_image": reference_image, + "version": version + } + + try: + response_data = self._make_request("/v1/image/edit", payload) + + # Create metadata from response fields + metadata = { + "version": response_data.get("version"), + "content_violation": response_data.get("content_violation"), + "prompt": prompt, + "reference_image": reference_image + } + + return ReveResponse( + success=True, + image=response_data.get("image"), + metadata=metadata, + request_id=response_data.get("request_id"), + credits_used=response_data.get("credits_used"), + credits_remaining=response_data.get("credits_remaining") + ) + except ReveError as e: + return ReveResponse( + success=False, + error_code=e.error_code, + error_message=str(e), + request_id=e.request_id + ) + + def remix(self, prompt: str, reference_images: List[str], aspect_ratio: str = "3:2", + version: str = "latest") -> ReveResponse: + """ + Remix multiple images with text guidance. + + Args: + prompt: Text description with XML tags for image references + reference_images: List of 1-4 base64 encoded images + aspect_ratio: Output image aspect ratio + version: Model version (latest, reve-remix@20250915) + + + Returns: + ReveResponse with remixed image and metadata + """ + if not reference_images or len(reference_images) > 4: + return ReveResponse( + success=False, + error_code="INVALID_PARAMETERS", + error_message="reference_images must contain 1-4 images" + ) + + payload = { + "prompt": prompt, + "reference_images": reference_images, + "aspect_ratio": aspect_ratio, + "version": version + } + + try: + response_data = self._make_request("/v1/image/remix", payload) + + # Create metadata from response fields + metadata = { + "version": response_data.get("version"), + "content_violation": response_data.get("content_violation"), + "aspect_ratio": aspect_ratio, + "prompt": prompt, + "reference_images": reference_images + } + + return ReveResponse( + success=True, + image=response_data.get("image"), + metadata=metadata, + request_id=response_data.get("request_id"), + credits_used=response_data.get("credits_used"), + credits_remaining=response_data.get("credits_remaining") + ) + except ReveError as e: + return ReveResponse( + success=False, + error_code=e.error_code, + error_message=str(e), + request_id=e.request_id + ) + + def get_credits(self) -> Dict[str, Any]: + """ + Get current credit balance (if supported by API). + + Returns: + Dictionary with credit information + """ + # This endpoint may not exist - implement if available + try: + response_data = self._make_request("/api/credits", {}) + return response_data + except ReveError: + # Fallback - credits are returned with each operation + return {"error": "Credits endpoint not available"} + + def close(self): + """Close the HTTP session.""" + if self.session: + self.session.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +# Utility function for easy client creation +def create_reve_client(api_key: str, timeout: int = 60) -> ReveAPIClient: + """ + Create a Reve API client instance. + + Args: + api_key: API key (required) + timeout: Request timeout in seconds + + Returns: + Configured ReveAPIClient instance + """ + return ReveAPIClient(api_key=api_key, timeout=timeout) diff --git a/docs/api_research.md b/docs/api_research.md new file mode 100644 index 0000000..8d324df --- /dev/null +++ b/docs/api_research.md @@ -0,0 +1,208 @@ +# Reve API Research and Specifications + +## Overview + +Reve API provides image generation, editing, and remixing capabilities through three main endpoints. This document consolidates the available information about the API structure, authentication, and operation parameters. + +## Base API Information + +- **Base URL**: `https://api.reve.com` +- **Authentication**: Bearer token via Authorization header +- **Content Type**: `application/json` +- **Response Format**: JSON with base64 encoded images + +## Authentication + +### API Key Management +- **Header Format**: `Authorization: Bearer {api_key}` +- **Environment Variable**: `REVE_API_KEY` +- **Scope**: All endpoints require authentication + +### Error Responses +- **401 Unauthorized**: Invalid or missing API key +- **403 Forbidden**: Insufficient permissions +- **429 Too Many Requests**: Rate limiting exceeded + +## API Endpoints + +### 1. Create Endpoint +**Purpose**: Generate images from text prompts + +``` +POST /api/create +``` + +**Parameters**: +- `prompt` (required): Text description of desired image +- `aspect_ratio` (optional): Image dimensions (16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 1:1) +- `version` (optional): Model version (latest, reve-create@20250915) +- `output_format` (optional): Image format (png, jpeg, webp, json) + +**Cost**: 18 credits per generation + +### 2. Edit Endpoint +**Purpose**: Modify existing images with text instructions + +``` +POST /api/edit +``` + +**Parameters**: +- `prompt` (required): Text instruction for editing +- `reference_image` (required): Base64 encoded source image +- `version` (optional): Model version (latest, reve-edit@20250915) +- `output_format` (optional): Image format (png, jpeg, webp, json) + +**Cost**: 30 credits per edit + +### 3. Remix Endpoint +**Purpose**: Combine multiple images with text guidance + +``` +POST /api/remix +``` + +**Parameters**: +- `prompt` (required): Text description with XML tags for image references +- `reference_images` (required): Array of 1-4 base64 encoded images +- `aspect_ratio` (optional): Output image dimensions +- `version` (optional): Model version (latest, reve-remix@20250915) +- `output_format` (optional): Image format (png, jpeg, webp, json) + +**Cost**: 30 credits per remix + +**XML Tag Format**: Use ``, ``, etc. in prompt to reference images + +## Response Format + +### Success Response +```json +{ + "image": "base64_encoded_image_data", + "request_id": "unique_request_identifier", + "credits_used": 18, + "credits_remaining": 982, + "metadata": { + "operation": "create|edit|remix", + "parameters": {...}, + "processing_time": 2.5, + "model_version": "reve-create@20250915" + } +} +``` + +### Error Response +```json +{ + "error": { + "code": "CONTENT_POLICY_VIOLATION", + "message": "Content violates policy guidelines", + "details": "Specific violation description" + }, + "request_id": "unique_request_identifier" +} +``` + +## Error Codes + +### Client Errors (4xx) +- `400 BAD_REQUEST`: Invalid parameters or malformed request +- `401 UNAUTHORIZED`: Invalid or missing API key +- `403 FORBIDDEN`: Insufficient permissions or credits +- `404 NOT_FOUND`: Endpoint not found +- `429 TOO_MANY_REQUESTS`: Rate limit exceeded + +### Server Errors (5xx) +- `500 INTERNAL_SERVER_ERROR`: Server processing error +- `502 BAD_GATEWAY`: Upstream service error +- `503 SERVICE_UNAVAILABLE`: Service temporarily unavailable + +## Rate Limiting + +- **Default Limits**: Not explicitly documented +- **Headers**: Check `X-RateLimit-*` headers in responses +- **Retry Strategy**: Exponential backoff recommended + +## Content Policy + +### Prohibited Content +- Violence, gore, or harmful content +- Adult or sexual content +- Copyrighted material without permission +- Hate speech or discriminatory content + +### Detection +- Automatic content filtering on all operations +- Returns `CONTENT_POLICY_VIOLATION` error code +- No credits charged for policy violations + +## Image Specifications + +### Input Requirements +- **Format**: Base64 encoded (PNG, JPEG, WebP) +- **Size Limits**: + - Edit: Single image, max 10MB + - Remix: 1-4 images, max 5MB each +- **Resolution**: Recommended 512x512 to 2048x2048 + +### Output Specifications +- **Format**: PNG, JPEG, WebP, or JSON metadata +- **Quality**: High resolution, production ready +- **Encoding**: Base64 in JSON response + +## Model Versions + +### Available Versions +- `latest`: Always points to newest stable version +- `reve-create@20250915`: Specific create model version +- `reve-edit@20250915`: Specific edit model version +- `reve-remix@20250915`: Specific remix model version + +### Version Selection +- Default: `latest` for all operations +- Explicit: Specify exact version for reproducibility +- Compatibility: Older versions may be deprecated + +## Integration Notes + +### Best Practices +1. **Error Handling**: Implement comprehensive error handling for all response codes +2. **Retry Logic**: Use exponential backoff for rate limiting and server errors +3. **Credit Monitoring**: Track credit usage to prevent service interruption +4. **Image Optimization**: Compress images before base64 encoding to reduce payload size +5. **Caching**: Cache successful responses when appropriate + +### Performance Considerations +- **Request Size**: Minimize base64 payload size +- **Concurrent Requests**: Respect rate limits +- **Timeout Handling**: Set appropriate timeouts (30-60 seconds) +- **Memory Management**: Handle large image data efficiently + +## Security Considerations + +### API Key Protection +- Store API keys securely (environment variables, secure storage) +- Never expose keys in client-side code +- Rotate keys regularly +- Monitor for unauthorized usage + +### Data Privacy +- Images are processed server-side +- No guarantee of data retention policies +- Consider data sensitivity before uploading + +## Testing and Development + +### Sandbox Environment +- No explicit sandbox mentioned in documentation +- Use low-cost operations for testing +- Monitor credit usage during development + +### Debugging +- Use `request_id` for support requests +- Log API responses for troubleshooting +- Monitor error patterns and rates + +--- + +*Note: This documentation is based on available project specifications and common API patterns. Some details may need verification against the actual Reve API documentation when accessible.* diff --git a/docs/unified_architecture.md b/docs/unified_architecture.md new file mode 100644 index 0000000..df7f487 --- /dev/null +++ b/docs/unified_architecture.md @@ -0,0 +1,193 @@ +# ComfyUI Reve API Integration Node - Unified Architecture Design + +## Overview + +This document defines the architecture for a unified ComfyUI custom node that integrates all Reve API endpoints (Create, Edit, Remix) into a single, dynamic node with operation-specific inputs and outputs. + +## Core Design Principles + +1. **Unified Interface**: Single node handles all three operations (Create, Edit, Remix) +2. **Dynamic Inputs**: Show/hide parameters based on selected operation type +3. **Consistent Outputs**: Standardized output format across all operations +4. **Error Handling**: Unified error management with operation-specific context +5. **Credit Tracking**: Integrated credit monitoring and usage tracking + +## Node Architecture + +### Main Node Class: `ReveAPI` + +```python +class ReveAPI: + """ + Unified Reve API node supporting Create, Edit, and Remix operations + with dynamic input system and consistent output handling. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "operation": (["create", "edit", "remix"], {"default": "create"}), + "prompt": ("STRING", {"multiline": True, "default": "A beautiful landscape"}) + }, + "optional": { + "api_key": ("STRING", {"default": ""}), + "version": (["latest", "reve-create@20250915", "reve-edit@20250915", "reve-remix@20250915"], + {"default": "latest"}), + "aspect_ratio": (["16:9", "9:16", "3:2", "2:3", "4:3", "3:4", "1:1"], + {"default": "3:2"}), + "output_format": (["png", "jpeg", "webp", "json"], {"default": "png"}), + "reference_image": ("IMAGE",), + "reference_images": ("IMAGE",) # Multi-image input for Remix + } + } + + RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT", "INT") + RETURN_NAMES = ("image", "metadata", "request_id", "credits_used", "credits_remaining") + FUNCTION = "process_request" + CATEGORY = "image/ai" +``` + +### Dynamic Input System + +The node implements conditional input visibility based on operation type: + +#### Create Mode +- **Show**: prompt, aspect_ratio, version, output_format, api_key +- **Hide**: reference_image, reference_images +- **Validation**: prompt required, aspect_ratio valid, version valid + +#### Edit Mode +- **Show**: prompt, reference_image, version, output_format, api_key +- **Hide**: reference_images, aspect_ratio +- **Validation**: prompt required, reference_image required, version valid + +#### Remix Mode +- **Show**: prompt, reference_images, aspect_ratio, version, output_format, api_key +- **Hide**: reference_image +- **Validation**: prompt required, reference_images (1-4), aspect_ratio valid, XML tags valid + +### Module Structure + +``` +custom_nodes/ComfyUI_Reve-API/ +├── __init__.py # Node registration and ComfyUI integration +├── requirements.txt # Dependencies +├── README.md # Installation and usage guide +├── nodes/ +│ ├── __init__.py +│ ├── reve_api_node.py # Main unified ReveAPI node +│ ├── dynamic_inputs.py # Dynamic input system implementation +│ └── operation_handlers.py # Operation-specific logic handlers +├── core/ +│ ├── __init__.py +│ ├── reve_api_client.py # HTTP client for Reve API +│ ├── image_utils.py # Image conversion utilities +│ ├── credit_monitor.py # Credit tracking and monitoring +│ └── error_handler.py # Error handling and content policy detection +├── docs/ +│ ├── api_research.md # Reve API specifications +│ ├── unified_architecture.md # This document +│ └── examples/ # Usage examples and workflows +└── tests/ + ├── __init__.py + ├── test_api_client.py + ├── test_image_utils.py + ├── test_unified_node.py + ├── test_dynamic_inputs.py + ├── test_operation_handlers.py + └── test_error_handling.py +``` + +### Core Components + +#### 1. API Client (`reve_api_client.py`) +- HTTP request handling with authentication +- Request formatting for each operation type +- Response parsing and error handling +- Retry logic for rate limiting +- Credit tracking integration + +#### 2. Image Utilities (`image_utils.py`) +- ComfyUI tensor to base64 conversion +- Multi-image handling for Remix operation +- Format validation and size checking +- Quality preservation during conversion + +#### 3. Dynamic Input System (`dynamic_inputs.py`) +- Operation-based input visibility control +- Parameter validation per operation type +- Input mapping and transformation +- ComfyUI refresh handling + +#### 4. Operation Handlers (`operation_handlers.py`) +- Create operation: Text-to-image generation +- Edit operation: Image editing with instructions +- Remix operation: Multi-image combination with XML parsing +- Unified response processing + +### API Integration Specifications + +#### Reve API Endpoints +- **Create**: `POST /api/create` - Text-to-image generation (18 credits) +- **Edit**: `POST /api/edit` - Image editing (30 credits) +- **Remix**: `POST /api/remix` - Multi-image combination (30 credits) + +#### Authentication +- API key via header: `Authorization: Bearer {api_key}` +- Environment variable fallback: `REVE_API_KEY` +- Per-request override via node input + +#### Request/Response Format +All operations return standardized response: +```json +{ + "image": "base64_encoded_image", + "request_id": "unique_request_identifier", + "credits_used": 18, + "credits_remaining": 982, + "metadata": { + "operation": "create|edit|remix", + "parameters": {...}, + "processing_time": 2.5 + } +} +``` + +### Error Handling Strategy + +#### Error Categories +1. **Authentication Errors** (401): Invalid API key +2. **Validation Errors** (400): Invalid parameters +3. **Content Policy** (400): Content policy violations +4. **Rate Limiting** (429): Too many requests +5. **Server Errors** (500): API service issues + +#### Error Response Format +```python +{ + "success": False, + "error_code": "CONTENT_POLICY_VIOLATION", + "error_message": "Content violates policy guidelines", + "operation": "create", + "retry_after": None # For rate limiting +} +``` + +### Performance Considerations + +1. **API Client Caching**: Reuse HTTP client instances +2. **Image Conversion Optimization**: Efficient tensor/base64 conversion +3. **Memory Management**: Cleanup after operation switching +4. **Parameter Validation**: Early validation to prevent wasted credits +5. **Lazy Loading**: Dynamic input system with minimal overhead + +### User Experience Features + +1. **Operation Mode Indicator**: Visual feedback in node title +2. **Parameter Tooltips**: Context-sensitive help text +3. **Credit Cost Preview**: Show estimated cost before execution +4. **Progress Indicators**: Real-time processing feedback +5. **Smart Parameter Preservation**: Maintain compatible settings when switching operations + +This architecture provides a solid foundation for implementing the unified Reve API integration node with all required functionality while maintaining ComfyUI best practices and user experience standards. diff --git a/docs/verbose_logging_example.md b/docs/verbose_logging_example.md new file mode 100644 index 0000000..32bcd99 --- /dev/null +++ b/docs/verbose_logging_example.md @@ -0,0 +1,133 @@ +# Verbose Logging Example + +This document demonstrates how to use the verbose logging feature in the ComfyUI Reve API node. + +## Basic Usage + +1. **Add the Reve API node** to your workflow from the `image/ai` category +2. **Set the operation** to your desired type (create, edit, or remix) +3. **Enable verbose logging** by setting the `verbose_logging` parameter to `True` +4. **Configure your other parameters** as needed +5. **Execute the workflow** and check the ComfyUI console for detailed logs + +## Example Workflow Setup + +### Create Operation with Verbose Logging + +``` +[Text Input] → prompt: "A beautiful sunset over mountains" +[Dropdown] → operation: "create" +[Dropdown] → aspect_ratio: "16:9" +[Checkbox] → verbose_logging: True +``` + +### Expected Console Output + +When you run this workflow, you'll see detailed logging in the ComfyUI console: + +``` +2025-09-16 12:38:26,317 - nodes.reve_api_node.verbose - INFO - 🚀 Starting Reve API CREATE operation +{ + "prompt_length": 35, + "operation": "create", + "version": "latest", + "aspect_ratio": "16:9", + "output_format": "png", + "has_reference_image": false, + "reference_images_count": 0 +} + +2025-09-16 12:38:28,842 - nodes.reve_api_node.verbose - INFO - 🔍 Reve API CREATE Operation Complete +{ + "operation": "create", + "request_id": "req-abc123def456", + "credits_used": 18, + "credits_remaining": 482, + "processing_time_seconds": 2.525, + "success": true, + "request_parameters": { + "prompt_length": 35, + "version": "latest", + "aspect_ratio": "16:9", + "has_reference_image": false, + "reference_images_count": 0 + } +} + +2025-09-16 12:38:28,845 - nodes.reve_api_node.verbose - INFO - ✅ Reve API CREATE operation completed successfully +{ + "total_processing_time_seconds": 2.528, + "api_call_time_seconds": 2.525, + "image_processing_time_seconds": 0.003 +} +``` + +## Understanding the Log Output + +### Operation Start Log +- **prompt_length**: Number of characters in your prompt +- **operation**: The type of operation being performed +- **version**: API version being used +- **aspect_ratio**: Selected aspect ratio +- **output_format**: Image format +- **has_reference_image**: Whether a reference image is provided +- **reference_images_count**: Number of reference images (for remix) + +### Operation Complete Log +- **request_id**: Unique identifier for tracking this API call +- **credits_used**: Number of credits consumed by this operation +- **credits_remaining**: Your remaining credit balance +- **processing_time_seconds**: Time taken by the API to process the request +- **success**: Whether the operation completed successfully +- **request_parameters**: Summary of the parameters sent to the API + +### Performance Summary Log +- **total_processing_time_seconds**: Total time from start to finish +- **api_call_time_seconds**: Time spent waiting for the API response +- **image_processing_time_seconds**: Time spent converting and processing the image + +## Error Logging Example + +If an error occurs, you'll see detailed error information: + +``` +2025-09-16 12:40:15,123 - nodes.reve_api_node.verbose - INFO - ❌ Reve API CREATE operation failed +{ + "error_type": "ReveAuthenticationError", + "error_message": "Invalid API key", + "operation": "create", + "time_to_failure_seconds": 0.245 +} +``` + +## Use Cases for Verbose Logging + +### 1. Debugging API Issues +Enable verbose logging when troubleshooting API connectivity or authentication problems. + +### 2. Performance Optimization +Monitor processing times to identify bottlenecks in your workflows. + +### 3. Credit Management +Track credit usage across different operations to optimize costs. + +### 4. Workflow Documentation +Create detailed logs of your workflow executions for documentation or auditing. + +### 5. Support and Troubleshooting +Provide detailed logs when seeking support for issues. + +## Best Practices + +1. **Enable only when needed**: Keep verbose logging disabled for production workflows to reduce console clutter +2. **Monitor credit usage**: Use the credit tracking information to manage your API usage +3. **Performance analysis**: Use timing information to optimize your workflows +4. **Error diagnosis**: Enable verbose logging when experiencing issues for better error information + +## Performance Impact + +- **Disabled (default)**: No performance impact +- **Enabled**: Minimal overhead (~1-5ms per operation for logging) +- **Log processing**: Structured JSON formatting adds negligible processing time + +The verbose logging feature is designed to have minimal impact on your workflow performance while providing maximum insight into the API operations. diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..0c18b23 --- /dev/null +++ b/nodes/__init__.py @@ -0,0 +1 @@ +# ComfyUI Reve API Integration - Nodes Module diff --git a/nodes/reve_api_node.py b/nodes/reve_api_node.py new file mode 100644 index 0000000..3fde08b --- /dev/null +++ b/nodes/reve_api_node.py @@ -0,0 +1,398 @@ +""" +Unified Reve API Node for ComfyUI + +This module implements a single unified node that handles all three Reve API operations +(Create, Edit, Remix) with dynamic inputs based on the selected operation type. +""" + +import os +import sys +import logging +import time +import json +from typing import Dict, Any, List, Tuple, Optional + +# Add the parent directory to the path to import our modules +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) + +try: + from core.reve_api_client import ReveAPIClient, ReveResponse, ReveError + from core.image_utils import ( + prepare_image_for_api, + prepare_multiple_images_for_api, + base64_to_tensor, + ImageConversionError + ) +except ImportError as e: + print(f"Failed to import Reve API modules: {e}") + raise + +import torch + +# Configure logging +logger = logging.getLogger(__name__) + +# Configure verbose logging for Reve API operations +verbose_logger = logging.getLogger(f"{__name__}.verbose") +verbose_logger.setLevel(logging.INFO) + +# Create formatter for verbose logs +verbose_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +# Only add handler if it doesn't exist to avoid duplicates +if not verbose_logger.handlers: + # Create console handler for verbose logging + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(verbose_formatter) + verbose_logger.addHandler(console_handler) + + # Prevent propagation to avoid duplicate logs + verbose_logger.propagate = False + + +class ReveAPI: + """ + Unified Reve API node supporting Create, Edit, and Remix operations + with dynamic input system and consistent output handling. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "operation": (["create", "edit", "remix"], { + "default": "create", + "tooltip": "Select the Reve API operation to perform" + }), + "prompt": ("STRING", { + "multiline": True, + "default": "A beautiful landscape", + "tooltip": "Text description for the operation" + }), + "api_key": ("STRING", { + "default": "", + "tooltip": "Reve API key - paste your API key here" + }) + }, + "optional": { + "version": (["latest", "reve-create@20250915", "reve-edit@20250915", "reve-remix@20250915"], { + "default": "latest", + "tooltip": "Model version to use" + }), + "aspect_ratio": (["16:9", "9:16", "3:2", "2:3", "4:3", "3:4", "1:1"], { + "default": "3:2", + "tooltip": "Output image aspect ratio (for Create and Remix operations)" + }), + "output_format": (["png", "jpeg", "webp"], { + "default": "png", + "tooltip": "Output image format" + }), + "reference_image": ("IMAGE", { + "tooltip": "Reference image for Edit operation" + }), + "reference_images": ("IMAGE", { + "tooltip": "Reference images for Remix operation (1-4 images)" + }), + "verbose_logging": ("BOOLEAN", { + "default": False, + "tooltip": "Enable verbose logging for API interactions, performance metrics, and debugging" + }) + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "metadata") + FUNCTION = "process_request" + CATEGORY = "image/ai" + DESCRIPTION = "Unified Reve API node for Create, Edit, and Remix operations with dynamic inputs" + + def __init__(self): + self._client = None + + def _log_verbose(self, message: str, data: Dict[str, Any] = None, verbose_enabled: bool = False): + """Log verbose information if verbose logging is enabled.""" + if verbose_enabled: + if data: + formatted_data = json.dumps(data, indent=2, default=str) + verbose_logger.info(f"{message}\n{formatted_data}") + else: + verbose_logger.info(message) + + def _log_api_interaction(self, operation: str, request_params: Dict[str, Any], + response: 'ReveResponse', processing_time: float, verbose_enabled: bool = False): + """Log detailed API interaction information.""" + if not verbose_enabled: + return + + interaction_data = { + "operation": operation, + "request_id": response.request_id, + "credits_used": response.credits_used, + "credits_remaining": response.credits_remaining, + "processing_time_seconds": round(processing_time, 3), + "success": response.success, + "request_parameters": { + "prompt_length": len(request_params.get("prompt", "")), + "version": request_params.get("version", "unknown"), + "aspect_ratio": request_params.get("aspect_ratio", "unknown"), + "has_reference_image": "reference_image" in request_params, + "reference_images_count": len(request_params.get("reference_images", [])) if "reference_images" in request_params else 0 + } + } + + if response.error_message: + interaction_data["error"] = { + "code": response.error_code, + "message": response.error_message + } + + self._log_verbose(f"🔍 Reve API {operation.upper()} Operation Complete", interaction_data, verbose_enabled) + + def _get_client(self, api_key: str = "") -> ReveAPIClient: + """Get or create API client instance.""" + if self._client is None: + self._client = ReveAPIClient(api_key=api_key if api_key else None) + return self._client + + def _validate_inputs(self, operation: str, **kwargs) -> Dict[str, Any]: + """Validate inputs based on operation type.""" + errors = [] + + if operation == "edit": + if "reference_image" not in kwargs or kwargs["reference_image"] is None: + errors.append("Edit operation requires a reference_image") + + elif operation == "remix": + if "reference_images" not in kwargs or kwargs["reference_images"] is None: + errors.append("Remix operation requires reference_images") + elif hasattr(kwargs["reference_images"], "shape"): + # Check if it's a batch of images + batch_size = kwargs["reference_images"].shape[0] + if batch_size < 1 or batch_size > 4: + errors.append(f"Remix operation requires 1-4 reference images, got {batch_size}") + + if errors: + raise ValueError("; ".join(errors)) + + return kwargs + + def _prepare_create_request(self, prompt: str, aspect_ratio: str, version: str, + output_format: str) -> Dict[str, Any]: + """Prepare request parameters for Create operation.""" + return { + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "version": version, + "output_format": output_format + } + + def _prepare_edit_request(self, prompt: str, reference_image: torch.Tensor, + version: str, output_format: str) -> Dict[str, Any]: + """Prepare request parameters for Edit operation.""" + try: + # Convert reference image to base64 + base64_image = prepare_image_for_api(reference_image, output_format) + + return { + "prompt": prompt, # This will be converted to 'edit_instruction' in the API client + "reference_image": base64_image, + "version": version + } + except ImageConversionError as e: + raise ValueError(f"Failed to prepare reference image: {str(e)}") + + def _prepare_remix_request(self, prompt: str, reference_images: torch.Tensor, + aspect_ratio: str, version: str, output_format: str) -> Dict[str, Any]: + """Prepare request parameters for Remix operation.""" + try: + # Handle batch of images + if reference_images.dim() == 4: + # Split batch into individual images + image_list = [reference_images[i:i+1] for i in range(reference_images.shape[0])] + else: + # Single image + image_list = [reference_images.unsqueeze(0)] + + # Convert all images to base64 + base64_images = prepare_multiple_images_for_api(image_list, output_format) + + return { + "prompt": prompt, + "reference_images": base64_images, + "aspect_ratio": aspect_ratio, + "version": version + } + except ImageConversionError as e: + raise ValueError(f"Failed to prepare reference images: {str(e)}") + + def _process_response(self, response: ReveResponse) -> Tuple[torch.Tensor, str]: + """Process API response and convert to ComfyUI outputs.""" + if not response.success: + error_msg = f"Reve API Error: {response.error_message}" + if response.error_code: + error_msg = f"{response.error_code}: {response.error_message}" + raise RuntimeError(error_msg) + + try: + # Convert base64 image to tensor + if response.image: + image_tensor = base64_to_tensor(response.image) + else: + raise ValueError("No image data in response") + + # Prepare metadata string (enhanced with additional information) + metadata = { + "operation": response.metadata.get("operation", "unknown") if response.metadata else "unknown", + "model_version": response.metadata.get("model_version", "unknown") if response.metadata else "unknown", + "processing_time": response.metadata.get("processing_time", 0) if response.metadata else 0, + "parameters": response.metadata.get("parameters", {}) if response.metadata else {}, + "request_id": response.request_id or "unknown", + "success": response.success + } + metadata_str = json.dumps(metadata, indent=2) + + return ( + image_tensor, + metadata_str + ) + + except Exception as e: + raise RuntimeError(f"Failed to process response: {str(e)}") + + def process_request(self, operation: str, prompt: str, api_key: str, + version: str = "latest", aspect_ratio: str = "3:2", + output_format: str = "png", reference_image: torch.Tensor = None, + reference_images: torch.Tensor = None, verbose_logging: bool = False) -> Tuple[torch.Tensor, str]: + """ + Process Reve API request based on operation type. + + Args: + operation: Operation type (create, edit, remix) + prompt: Text description/instruction + api_key: Reve API key (required) + version: Model version + aspect_ratio: Output aspect ratio + output_format: Output format + reference_image: Single reference image for edit + reference_images: Multiple reference images for remix + verbose_logging: Enable verbose logging for debugging and monitoring + + Returns: + Tuple of (image_tensor, metadata_str) + + Note: + When verbose_logging is enabled, additional information including request_id, + credits_used, credits_remaining, and performance metrics are logged to the console. + """ + try: + # Validate API key is provided + if not api_key or api_key.strip() == "": + raise ValueError("API key is required. Please paste your Reve API key in the api_key field.") + + # Log start of operation if verbose logging is enabled + start_time = time.time() + self._log_verbose(f"🚀 Starting Reve API {operation.upper()} operation", { + "prompt_length": len(prompt), + "operation": operation, + "version": version, + "aspect_ratio": aspect_ratio, + "output_format": output_format, + "has_reference_image": reference_image is not None, + "reference_images_count": reference_images.shape[0] if reference_images is not None else 0 + }, verbose_logging) + + # Validate inputs based on operation + inputs = { + "reference_image": reference_image, + "reference_images": reference_images + } + self._validate_inputs(operation, **inputs) + + # Get API client + client = self._get_client(api_key) + + # Prepare request based on operation type + api_start_time = time.time() + request_params = {} + + if operation == "create": + request_params = { + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "version": version + } + response = client.create(**request_params) + + elif operation == "edit": + request_params = self._prepare_edit_request( + prompt, reference_image, version, output_format + ) + response = client.edit(**request_params) + + elif operation == "remix": + request_params = self._prepare_remix_request( + prompt, reference_images, aspect_ratio, version, output_format + ) + response = client.remix(**request_params) + + else: + raise ValueError(f"Unsupported operation: {operation}") + + # Calculate processing time + processing_time = time.time() - api_start_time + total_time = time.time() - start_time + + # Log API interaction details + self._log_api_interaction(operation, request_params, response, processing_time, verbose_logging) + + # Log completion summary + self._log_verbose(f"✅ Reve API {operation.upper()} operation completed successfully", { + "total_processing_time_seconds": round(total_time, 3), + "api_call_time_seconds": round(processing_time, 3), + "image_processing_time_seconds": round(total_time - processing_time, 3) + }, verbose_logging) + + # Process and return response + return self._process_response(response) + + except Exception as e: + # Calculate error time for verbose logging + error_time = time.time() - start_time if 'start_time' in locals() else 0 + + # Log error details if verbose logging is enabled + self._log_verbose(f"❌ Reve API {operation.upper()} operation failed", { + "error_type": type(e).__name__, + "error_message": str(e), + "operation": operation, + "time_to_failure_seconds": round(error_time, 3) + }, verbose_logging) + + logger.error(f"Reve API request failed: {str(e)}") + + # Provide user-friendly error messages for common issues + error_str = str(e) + if "API_NOT_AVAILABLE" in error_str or "ENDPOINT_NOT_FOUND" in error_str: + raise RuntimeError( + "❌ Reve API is not currently available.\n\n" + "The Reve API endpoints are not accessible. This could be because:\n" + "• The API is still in development\n" + "• The API requires special access or activation\n" + "• The API endpoints have changed\n\n" + "Please visit https://api.reve.com or contact Reve support for the latest API status.\n" + "You can use the Reve web interface at https://app.reve.com in the meantime." + ) + else: + raise RuntimeError(f"Reve API request failed: {str(e)}") + + def __del__(self): + """Cleanup API client on node destruction.""" + if self._client: + try: + self._client.close() + except: + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71a159f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +# ComfyUI Reve API Integration Dependencies + +# HTTP client for API requests +requests>=2.28.0 + +# Image processing (should be available in ComfyUI environment) +Pillow>=9.0.0 + +# NumPy for array operations (should be available in ComfyUI environment) +numpy>=1.21.0 + +# PyTorch (should be available in ComfyUI environment) +# torch>=1.13.0 + +# Optional: For enhanced error handling and validation +# pydantic>=1.10.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..22e29e7 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# ComfyUI Reve API Integration - Tests Module diff --git a/workflow/reve_allin1.json b/workflow/reve_allin1.json new file mode 100644 index 0000000..c63397a --- /dev/null +++ b/workflow/reve_allin1.json @@ -0,0 +1,411 @@ +{ + "id": "a42eb435-91b1-4ff3-b0ed-f6a889436939", + "revision": 0, + "last_node_id": 14, + "last_link_id": 34, + "nodes": [ + { + "id": 6, + "type": "LoadImage", + "pos": [ + -604.8377075195312, + 749.3304443359375 + ], + "size": [ + 270, + 314 + ], + "flags": {}, + "order": 0, + "mode": 4, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 19 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.3.59", + "Node name for S&R": "LoadImage", + "ue_properties": { + "version": "7.0.1", + "widget_ue_connectable": { + "image": true, + "upload": true + } + } + }, + "widgets_values": [ + "nogaruka._Photo_round_wooden_font_in_a_bathhouse_with_a_lot_of__1989a698-e36c-4e11-b885-24c2e3ccfda5.png", + "image" + ] + }, + { + "id": 8, + "type": "LoadImage", + "pos": [ + -314.837890625, + 899.3301391601562 + ], + "size": [ + 270, + 314.00006103515625 + ], + "flags": {}, + "order": 1, + "mode": 4, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 20 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.3.59", + "Node name for S&R": "LoadImage", + "ue_properties": { + "version": "7.0.1", + "widget_ue_connectable": { + "image": true, + "upload": true + } + } + }, + "widgets_values": [ + "eclissidilana_65569_a_very_realistic_beautiful_night_flash_film_d631d5c6-f601-4d1e-b61a-af4352aab2de (2).png", + "image" + ] + }, + { + "id": 13, + "type": "LoadImage", + "pos": [ + -309.6622314453125, + 333.55999755859375 + ], + "size": [ + 270, + 314.0000305175781 + ], + "flags": {}, + "order": 2, + "mode": 4, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 32 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.3.59", + "Node name for S&R": "LoadImage", + "ue_properties": { + "version": "7.0.1" + } + }, + "widgets_values": [ + "yo9otatara_A_portrait_of_an_bald_man_wearing_a_white_turtlene_5e874a40-107e-4611-b5b5-6edc4873378b_0.png", + "image" + ] + }, + { + "id": 10, + "type": "ImageConcanate", + "pos": [ + -314.837890625, + 749.3304443359375 + ], + "size": [ + 270, + 102 + ], + "flags": {}, + "order": 3, + "mode": 4, + "inputs": [ + { + "name": "image1", + "type": "IMAGE", + "link": 19 + }, + { + "name": "image2", + "type": "IMAGE", + "link": 20 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 33 + ] + } + ], + "properties": { + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfyui-kjnodes", + "ver": "1.1.4", + "Node name for S&R": "ImageConcanate", + "ue_properties": { + "version": "7.0.1" + } + }, + "widgets_values": [ + "right", + true + ] + }, + { + "id": 3, + "type": "PreviewImage", + "pos": [ + 463.3653564453125, + 435.236572265625 + ], + "size": [ + 498.1344909667969, + 372.80816650390625 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 34 + } + ], + "outputs": [], + "properties": { + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.3.59", + "Node name for S&R": "PreviewImage", + "ue_properties": { + "version": "7.0.1", + "widget_ue_connectable": {} + } + }, + "widgets_values": [] + }, + { + "id": 14, + "type": "ReveAPI", + "pos": [ + -5.167802333831787, + 457.3710632324219 + ], + "size": [ + 408.34271240234375, + 544.4044189453125 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "reference_image", + "shape": 7, + "type": "IMAGE", + "link": 32 + }, + { + "name": "reference_images", + "shape": 7, + "type": "IMAGE", + "link": 33 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 34 + ] + }, + { + "name": "metadata", + "type": "STRING", + "links": null + } + ], + "properties": { + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "Node name for S&R": "ReveAPI", + "ue_properties": { + "version": "7.0.1" + } + }, + "widgets_values": [ + "create", + "macro photograph of a human eye with a green iris, golden flecks, and delicate sunflower petals integrated seamlessly inside the pupil, surrounding eyelid adorned with soft pastel makeup, moist realistic skin detail, cinematic lighting, no text or symbols.", + "papi.2a668e8b-9fa1-472e-b773-816095cea5d7.N5jZ6CUA_0_TmyB-c7a3Dr9fATWuvZtN", + "latest", + "3:2", + "png", + true + ] + } + ], + "links": [ + [ + 19, + 6, + 0, + 10, + 0, + "IMAGE" + ], + [ + 20, + 8, + 0, + 10, + 1, + "IMAGE" + ], + [ + 32, + 13, + 0, + 14, + 0, + "IMAGE" + ], + [ + 33, + 10, + 0, + 14, + 1, + "IMAGE" + ], + [ + 34, + 14, + 0, + 3, + 0, + "IMAGE" + ] + ], + "groups": [ + { + "id": 1, + "title": "Remix+", + "bounding": [ + -614.8377075195312, + 679.3304443359375, + 580, + 547.5999755859375 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Edit+", + "bounding": [ + -319.662109375, + 259.9598693847656, + 290, + 397.6000061035156 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "ue_links": [], + "links_added_by_ue": [], + "ds": { + "scale": 1.0152559799477145, + "offset": [ + 570.2145821362822, + -28.935737510802966 + ] + }, + "frontendVersion": "1.26.11", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file