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
This commit is contained in:
@@ -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
|
||||
+170
@@ -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/
|
||||
@@ -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 <repository_url> 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
|
||||
+48
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
# ComfyUI Reve API Integration - Core Module
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 `<img1>`, `<img2>`, 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.*
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
# ComfyUI Reve API Integration - Nodes Module
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# ComfyUI Reve API Integration - Tests Module
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user