Enhance image processing and error handling in AICore and tasks
- Improved response parsing in AICore to handle both array and dictionary formats, including detailed error logging. - Updated image directory handling in tasks to prioritize web-accessible paths for image storage, with robust fallback mechanisms. - Adjusted image URL generation in serializers and frontend components to support new directory structure and ensure proper accessibility.
This commit is contained in:
@@ -729,25 +729,74 @@ class AICore:
|
||||
|
||||
body = response.json()
|
||||
print(f"[AI][{function_name}] Runware response type: {type(body)}, length: {len(body) if isinstance(body, list) else 'N/A'}")
|
||||
logger.info(f"[AI][{function_name}] Runware response body (first 1000 chars): {str(body)[:1000]}")
|
||||
|
||||
# Runware returns array: [auth_result, image_result]
|
||||
# image_result has 'data' array with image objects containing 'imageURL'
|
||||
# Reference: AIProcessor has more robust parsing - match that logic
|
||||
image_url = None
|
||||
error_msg = None
|
||||
|
||||
if isinstance(body, list):
|
||||
# Find the imageInference result (usually second element)
|
||||
for item in body:
|
||||
# Case 1: Array response - find the imageInference result
|
||||
print(f"[AI][{function_name}] Response is array with {len(body)} elements")
|
||||
for idx, item in enumerate(body):
|
||||
print(f"[AI][{function_name}] Array element {idx}: {type(item)}, keys: {list(item.keys()) if isinstance(item, dict) else 'N/A'}")
|
||||
if isinstance(item, dict):
|
||||
# Check for 'data' key (image result)
|
||||
if 'data' in item and isinstance(item['data'], list) and len(item['data']) > 0:
|
||||
image_data = item['data'][0]
|
||||
image_url = image_data.get('imageURL') or image_data.get('image_url')
|
||||
if image_url:
|
||||
break
|
||||
# Check for direct 'imageURL' (fallback)
|
||||
elif 'imageURL' in item:
|
||||
image_url = item.get('imageURL')
|
||||
if image_url:
|
||||
# Check if this is the image result with 'data' key
|
||||
if 'data' in item:
|
||||
data = item['data']
|
||||
print(f"[AI][{function_name}] Found 'data' key, type: {type(data)}")
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
first_item = data[0]
|
||||
print(f"[AI][{function_name}] First data item keys: {list(first_item.keys()) if isinstance(first_item, dict) else 'N/A'}")
|
||||
image_url = first_item.get('imageURL') or first_item.get('image_url')
|
||||
if image_url:
|
||||
print(f"[AI][{function_name}] Found imageURL: {image_url[:50]}...")
|
||||
break
|
||||
# Check for errors
|
||||
if 'errors' in item:
|
||||
errors = item['errors']
|
||||
print(f"[AI][{function_name}] Found 'errors' key, type: {type(errors)}")
|
||||
if isinstance(errors, list) and len(errors) > 0:
|
||||
error_obj = errors[0]
|
||||
error_msg = error_obj.get('message') or error_obj.get('error') or str(error_obj)
|
||||
print(f"[AI][{function_name}][Error] Error in response: {error_msg}")
|
||||
break
|
||||
# Check for error at root level
|
||||
if 'error' in item:
|
||||
error_msg = item['error']
|
||||
print(f"[AI][{function_name}][Error] Error at root level: {error_msg}")
|
||||
break
|
||||
elif isinstance(body, dict):
|
||||
# Case 2: Direct dict response
|
||||
print(f"[AI][{function_name}] Response is dict with keys: {list(body.keys())}")
|
||||
if 'data' in body:
|
||||
data = body['data']
|
||||
print(f"[AI][{function_name}] Found 'data' key, type: {type(data)}")
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
first_item = data[0]
|
||||
print(f"[AI][{function_name}] First data item keys: {list(first_item.keys()) if isinstance(first_item, dict) else 'N/A'}")
|
||||
image_url = first_item.get('imageURL') or first_item.get('image_url')
|
||||
elif 'errors' in body:
|
||||
errors = body['errors']
|
||||
print(f"[AI][{function_name}] Found 'errors' key, type: {type(errors)}")
|
||||
if isinstance(errors, list) and len(errors) > 0:
|
||||
error_obj = errors[0]
|
||||
error_msg = error_obj.get('message') or error_obj.get('error') or str(error_obj)
|
||||
print(f"[AI][{function_name}][Error] Error in response: {error_msg}")
|
||||
elif 'error' in body:
|
||||
error_msg = body['error']
|
||||
print(f"[AI][{function_name}][Error] Error at root level: {error_msg}")
|
||||
|
||||
if error_msg:
|
||||
print(f"[AI][{function_name}][Error] Runware API error: {error_msg}")
|
||||
return {
|
||||
'url': None,
|
||||
'provider': 'runware',
|
||||
'cost': 0.0,
|
||||
'error': error_msg,
|
||||
}
|
||||
|
||||
if image_url:
|
||||
|
||||
@@ -763,8 +812,10 @@ class AICore:
|
||||
'error': None,
|
||||
}
|
||||
else:
|
||||
error_msg = 'No image data in Runware response'
|
||||
# If we get here, we couldn't parse the response
|
||||
error_msg = f'No image data in Runware response. Response type: {type(body).__name__}'
|
||||
print(f"[AI][{function_name}][Error] {error_msg}")
|
||||
logger.error(f"[AI][{function_name}] Full Runware response: {json.dumps(body, indent=2) if isinstance(body, (dict, list)) else str(body)}")
|
||||
return {
|
||||
'url': None,
|
||||
'provider': 'runware',
|
||||
|
||||
@@ -537,12 +537,17 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
from pathlib import Path
|
||||
|
||||
# Create images directory if it doesn't exist
|
||||
# Use /data/app/igny8/images (mounted volume) for persistence
|
||||
# Fallback to /data/app/images if mounted path not available
|
||||
images_dir = '/data/app/igny8/images' # Use mounted volume path
|
||||
# Use frontend/public/images/ai-images/ for web accessibility (like /images/logo/)
|
||||
# This allows images to be served via app.igny8.com/images/ai-images/
|
||||
write_test_passed = False
|
||||
images_dir = None
|
||||
|
||||
# Try frontend/public/images/ai-images/ first (web-accessible)
|
||||
try:
|
||||
from django.conf import settings
|
||||
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path(__file__).resolve().parent.parent.parent
|
||||
# Navigate to frontend/public/images/ai-images/
|
||||
images_dir = str(base_dir / 'frontend' / 'public' / 'images' / 'ai-images')
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
# Test write access
|
||||
test_file = os.path.join(images_dir, '.write_test')
|
||||
@@ -550,33 +555,31 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
f.write('test')
|
||||
os.remove(test_file)
|
||||
write_test_passed = True
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Directory writable (mounted volume): {images_dir}")
|
||||
except Exception as write_test_error:
|
||||
logger.warning(f"[process_image_generation_queue] Image {image_id} - Mounted directory not writable: {images_dir}, error: {write_test_error}")
|
||||
# Fallback to /data/app/images
|
||||
images_dir = '/data/app/images'
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Directory writable (web-accessible): {images_dir}")
|
||||
except Exception as web_dir_error:
|
||||
logger.warning(f"[process_image_generation_queue] Image {image_id} - Web-accessible directory not writable: {images_dir}, error: {web_dir_error}")
|
||||
# Fallback to /data/app/igny8/images (mounted volume)
|
||||
try:
|
||||
images_dir = '/data/app/igny8/images'
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
test_file = os.path.join(images_dir, '.write_test')
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('test')
|
||||
os.remove(test_file)
|
||||
write_test_passed = True
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Using fallback directory (writable): {images_dir}")
|
||||
except Exception as fallback_error:
|
||||
logger.warning(f"[process_image_generation_queue] Image {image_id} - Fallback directory also not writable: {images_dir}, error: {fallback_error}")
|
||||
# Final fallback to project-relative path
|
||||
from django.conf import settings
|
||||
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path(__file__).resolve().parent.parent.parent
|
||||
images_dir = str(base_dir / 'data' / 'app' / 'images')
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Using mounted volume directory: {images_dir}")
|
||||
except Exception as mounted_error:
|
||||
logger.warning(f"[process_image_generation_queue] Image {image_id} - Mounted directory not writable: {images_dir}, error: {mounted_error}")
|
||||
# Final fallback to /data/app/images
|
||||
try:
|
||||
images_dir = '/data/app/images'
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
test_file = os.path.join(images_dir, '.write_test')
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('test')
|
||||
os.remove(test_file)
|
||||
write_test_passed = True
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Using project-relative directory (writable): {images_dir}")
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Using fallback directory: {images_dir}")
|
||||
except Exception as final_error:
|
||||
logger.error(f"[process_image_generation_queue] Image {image_id} - All directories not writable. Last error: {final_error}")
|
||||
raise Exception(f"None of the image directories are writable. Last error: {final_error}")
|
||||
@@ -584,10 +587,12 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
if not write_test_passed:
|
||||
raise Exception(f"Failed to find writable directory for saving images")
|
||||
|
||||
# Generate filename: image_{image_id}_{timestamp}.png
|
||||
# Generate filename: image_{image_id}_{timestamp}.png (or .webp for Runware)
|
||||
import time
|
||||
timestamp = int(time.time())
|
||||
filename = f"image_{image_id}_{timestamp}.png"
|
||||
# Use webp extension if provider is Runware, otherwise png
|
||||
file_ext = 'webp' if provider == 'runware' else 'png'
|
||||
filename = f"image_{image_id}_{timestamp}.{file_ext}"
|
||||
file_path = os.path.join(images_dir, filename)
|
||||
|
||||
# Download image
|
||||
|
||||
@@ -172,10 +172,19 @@ class ContentImageSerializer(serializers.ModelSerializer):
|
||||
def get_image_url(self, obj):
|
||||
"""
|
||||
Return proper HTTP URL for image.
|
||||
Priority: If image_path exists, return file endpoint URL, otherwise return image_url (API URL).
|
||||
Priority: If image_path exists and is in ai-images folder, return web URL,
|
||||
otherwise return file endpoint URL or image_url (API URL).
|
||||
"""
|
||||
if obj.image_path:
|
||||
# Return file endpoint URL for locally saved images
|
||||
# Check if path is in ai-images folder (web-accessible)
|
||||
if 'ai-images' in obj.image_path:
|
||||
# Extract filename from path
|
||||
filename = obj.image_path.split('ai-images/')[-1] if 'ai-images/' in obj.image_path else obj.image_path.split('ai-images\\')[-1]
|
||||
if filename:
|
||||
# Return web-accessible URL (like /images/logo/logo.svg)
|
||||
return f'/images/ai-images/{filename}'
|
||||
|
||||
# For other local paths, use file endpoint
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
# Build absolute URL for file endpoint
|
||||
|
||||
Reference in New Issue
Block a user