feat: migrate ComfyUI nodes from presigned URLs to Brain API storage endpoints ## Core Architecture Changes - Add new BrainApiClient for direct Brain API communication - Replace presigned URL workflow with storage_id/filename pattern - Update MediaStreamInput to use storage_id + filename instead of presigned_download_url - Update MediaStreamOutput to use storage_id + filename instead of presigned_upload_url ## Brain API Client Implementation - Add comprehensive BrainApiClient with authentication and error handling - Implement get_presigned_upload_url() and get_presigned_download_url() methods - Add proper Bearer token authentication for Brain API requests - Include environment variable configuration for Brain API endpoint and credentials ## Media Stream Node Updates - Refactor MediaStreamInput to request presigned URLs from Brain API using storage_id - Update MediaStreamOutput to upload directly to MinIO using Brain API presigned URLs - Simplify node interface by removing presigned URL inputs from workflow templates - Update JavaScript frontend to hide system inputs and manage node UI state ## Infrastructure Improvements - Add .idea/ to .gitignore for IDE file exclusions - Update user_input.py to use new storage_id pattern - Streamline web interface JavaScript for cleaner node management
373 lines
13 KiB
Python
373 lines
13 KiB
Python
"""
|
|
Brain API Client for ComfyUI Nodes
|
|
|
|
This client provides methods to interact with the Brain API storage endpoints,
|
|
replacing the need for pre-signed URLs in the ComfyUI workflow.
|
|
"""
|
|
|
|
import requests
|
|
import os
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
dotenv_path = os.path.join(current_dir, ".env")
|
|
load_dotenv(dotenv_path=dotenv_path)
|
|
|
|
# Setup logging
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
)
|
|
|
|
|
|
class BrainApiClient:
|
|
"""
|
|
Client for interacting with Brain API storage endpoints.
|
|
|
|
This client handles authentication and provides methods for uploading,
|
|
downloading, and deleting files through the Brain API storage endpoints.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the Brain API client with configuration from environment variables."""
|
|
self.base_url = os.getenv("BRANDO_BRAIN_API_BASE_URL", "http://localhost:2024/api")
|
|
self.api_key = os.getenv("BRANDO_API_KEY")
|
|
|
|
if not self.api_key:
|
|
raise ValueError(
|
|
"BRANDO_API_KEY environment variable is required for Brain API authentication"
|
|
)
|
|
|
|
self.headers = {
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
"User-Agent": "ComfyUI-NilorNodes/1.0"
|
|
}
|
|
|
|
logging.info(f"Brain API Client initialized with base URL: {self.base_url}")
|
|
|
|
def upload_file_to_storage(self, file_path: str, filename: str) -> Dict[str, Any]:
|
|
"""
|
|
Upload a file to Brain API storage and return storage metadata.
|
|
|
|
Args:
|
|
file_path: Local path to the file to upload
|
|
filename: Name to use for the uploaded file
|
|
|
|
Returns:
|
|
Dict containing storage_id and filename
|
|
|
|
Raises:
|
|
requests.RequestException: If upload fails
|
|
FileNotFoundError: If file_path doesn't exist
|
|
"""
|
|
if not os.path.exists(file_path):
|
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
|
|
url = f"{self.base_url}/storage/upload"
|
|
|
|
try:
|
|
with open(file_path, 'rb') as file:
|
|
files = {'file': (filename, file, 'application/octet-stream')}
|
|
|
|
logging.info(f"Uploading file '{filename}' to Brain API storage...")
|
|
response = requests.post(
|
|
url,
|
|
files=files,
|
|
headers=self.headers,
|
|
timeout=300
|
|
)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
logging.info(f"Upload successful. Storage ID: {result.get('storage_id')}")
|
|
return result
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to upload file '{filename}': {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error uploading file '{filename}': {e}")
|
|
raise
|
|
|
|
def upload_fileobj_to_storage(self, file_obj, filename: str, content_type: str = 'application/octet-stream') -> Dict[str, Any]:
|
|
"""
|
|
Upload a file-like object to Brain API storage and return storage metadata.
|
|
|
|
Args:
|
|
file_obj: File-like object to upload
|
|
filename: Name to use for the uploaded file
|
|
content_type: MIME type of the file
|
|
|
|
Returns:
|
|
Dict containing storage_id and filename
|
|
|
|
Raises:
|
|
requests.RequestException: If upload fails
|
|
"""
|
|
url = f"{self.base_url}/storage/upload"
|
|
|
|
try:
|
|
files = {'file': (filename, file_obj, content_type)}
|
|
|
|
logging.info(f"Uploading file object '{filename}' to Brain API storage...")
|
|
response = requests.post(
|
|
url,
|
|
files=files,
|
|
headers=self.headers,
|
|
timeout=300
|
|
)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
logging.info(f"Upload successful. Storage ID: {result.get('storage_id')}")
|
|
return result
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to upload file object '{filename}': {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error uploading file object '{filename}': {e}")
|
|
raise
|
|
|
|
def download_file_from_storage(self, storage_id: str, filename: str, dest_path: str) -> str:
|
|
"""
|
|
Download a file from Brain API storage to a local path.
|
|
|
|
Args:
|
|
storage_id: Storage ID of the file to download
|
|
filename: Name of the file to download
|
|
dest_path: Local path where the file should be saved
|
|
|
|
Returns:
|
|
Path to the downloaded file
|
|
|
|
Raises:
|
|
requests.RequestException: If download fails
|
|
"""
|
|
url = f"{self.base_url}/storage/{storage_id}"
|
|
params = {'filename': filename}
|
|
|
|
try:
|
|
logging.info(f"Downloading file '{filename}' (storage_id: {storage_id}) from Brain API storage...")
|
|
response = requests.get(
|
|
url,
|
|
params=params,
|
|
headers=self.headers,
|
|
timeout=300,
|
|
stream=True
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# Ensure destination directory exists
|
|
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
|
|
with open(dest_path, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
|
|
logging.info(f"Download successful. File saved to: {dest_path}")
|
|
return dest_path
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to download file '{filename}' (storage_id: {storage_id}): {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error downloading file '{filename}': {e}")
|
|
raise
|
|
|
|
def get_file_from_storage(self, storage_id: str, filename: str) -> bytes:
|
|
"""
|
|
Get file content from Brain API storage as bytes.
|
|
|
|
Args:
|
|
storage_id: Storage ID of the file to download
|
|
filename: Name of the file to download
|
|
|
|
Returns:
|
|
File content as bytes
|
|
|
|
Raises:
|
|
requests.RequestException: If download fails
|
|
"""
|
|
url = f"{self.base_url}/storage/{storage_id}"
|
|
params = {'filename': filename}
|
|
|
|
try:
|
|
logging.info(f"Getting file '{filename}' (storage_id: {storage_id}) from Brain API storage...")
|
|
response = requests.get(
|
|
url,
|
|
params=params,
|
|
headers=self.headers,
|
|
timeout=300
|
|
)
|
|
response.raise_for_status()
|
|
|
|
logging.info(f"File retrieval successful. Size: {len(response.content)} bytes")
|
|
return response.content
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to get file '{filename}' (storage_id: {storage_id}): {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error getting file '{filename}': {e}")
|
|
raise
|
|
|
|
def delete_file_from_storage(self, storage_id: str, filename: str) -> None:
|
|
"""
|
|
Delete a file from Brain API storage.
|
|
|
|
Args:
|
|
storage_id: Storage ID of the file to delete
|
|
filename: Name of the file to delete
|
|
|
|
Raises:
|
|
requests.RequestException: If deletion fails
|
|
"""
|
|
url = f"{self.base_url}/storage/{storage_id}"
|
|
params = {'filename': filename}
|
|
|
|
try:
|
|
logging.info(f"Deleting file '{filename}' (storage_id: {storage_id}) from Brain API storage...")
|
|
response = requests.delete(
|
|
url,
|
|
params=params,
|
|
headers=self.headers,
|
|
timeout=60
|
|
)
|
|
response.raise_for_status()
|
|
|
|
logging.info(f"File deletion successful")
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to delete file '{filename}' (storage_id: {storage_id}): {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error deleting file '{filename}': {e}")
|
|
raise
|
|
|
|
def get_presigned_upload_url(self, filename: str, content_type: str, minio_endpoint: str) -> Dict[str, Any]:
|
|
"""
|
|
Get a presigned upload URL from Brain API for direct MinIO upload.
|
|
|
|
Args:
|
|
filename: Name of the file to upload
|
|
content_type: MIME type of the file
|
|
minio_endpoint: MinIO endpoint that ComfyUI can access
|
|
|
|
Returns:
|
|
Dict containing storage_id, upload_url, and object_key
|
|
|
|
Raises:
|
|
requests.RequestException: If request fails
|
|
"""
|
|
url = f"{self.base_url}/storage/generate-upload-url"
|
|
payload = {
|
|
"filename": filename,
|
|
"content_type": content_type,
|
|
"minio_endpoint": minio_endpoint
|
|
}
|
|
|
|
try:
|
|
logging.info(f"Requesting presigned upload URL for '{filename}' from Brain API...")
|
|
response = requests.post(
|
|
url,
|
|
json=payload,
|
|
headers=self.headers,
|
|
timeout=30
|
|
)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
logging.info(f"Presigned upload URL generated. Storage ID: {result.get('storage_id')}")
|
|
return result
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to get presigned upload URL for '{filename}': {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error getting presigned upload URL for '{filename}': {e}")
|
|
raise
|
|
|
|
def get_presigned_download_url(self, storage_id: str, filename: str, minio_endpoint: str) -> Dict[str, Any]:
|
|
"""
|
|
Get a presigned download URL from Brain API for direct MinIO download.
|
|
|
|
Args:
|
|
storage_id: Storage ID of the file to download
|
|
filename: Name of the file to download
|
|
minio_endpoint: MinIO endpoint that ComfyUI can access
|
|
|
|
Returns:
|
|
Dict containing download_url
|
|
|
|
Raises:
|
|
requests.RequestException: If request fails
|
|
"""
|
|
url = f"{self.base_url}/storage/generate-download-url"
|
|
payload = {
|
|
"storage_id": storage_id,
|
|
"filename": filename,
|
|
"minio_endpoint": minio_endpoint
|
|
}
|
|
|
|
try:
|
|
logging.info(f"Requesting presigned download URL for '{filename}' (storage_id: {storage_id}) from Brain API...")
|
|
response = requests.post(
|
|
url,
|
|
json=payload,
|
|
headers=self.headers,
|
|
timeout=30
|
|
)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
logging.info(f"Presigned download URL generated for '{filename}'")
|
|
return result
|
|
|
|
except requests.RequestException as e:
|
|
logging.error(f"Failed to get presigned download URL for '{filename}' (storage_id: {storage_id}): {e}")
|
|
raise
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error getting presigned download URL for '{filename}': {e}")
|
|
raise
|
|
|
|
def health_check(self) -> bool:
|
|
"""
|
|
Check if the Brain API is accessible and authentication is working.
|
|
|
|
Returns:
|
|
True if API is accessible, False otherwise
|
|
"""
|
|
try:
|
|
# Try to access a simple endpoint to verify connectivity
|
|
url = f"{self.base_url}/health" # Assuming there's a health endpoint
|
|
response = requests.get(url, headers=self.headers, timeout=10)
|
|
return response.status_code == 200
|
|
except:
|
|
# If health endpoint doesn't exist, try the storage upload endpoint
|
|
# with a HEAD request to check authentication
|
|
try:
|
|
url = f"{self.base_url}/storage/upload"
|
|
response = requests.head(url, headers=self.headers, timeout=10)
|
|
return response.status_code in [200, 405] # 405 Method Not Allowed is OK for HEAD
|
|
except:
|
|
return False
|
|
|
|
|
|
# Global client instance
|
|
_brain_api_client = None
|
|
|
|
def get_brain_api_client() -> BrainApiClient:
|
|
"""
|
|
Get or create the global Brain API client instance.
|
|
|
|
Returns:
|
|
BrainApiClient instance
|
|
"""
|
|
global _brain_api_client
|
|
if _brain_api_client is None:
|
|
_brain_api_client = BrainApiClient()
|
|
return _brain_api_client
|