diff --git a/backend/igny8_core/ai/ai_core.py b/backend/igny8_core/ai/ai_core.py index 7fc878de..2ea810cb 100644 --- a/backend/igny8_core/ai/ai_core.py +++ b/backend/igny8_core/ai/ai_core.py @@ -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', diff --git a/backend/igny8_core/ai/tasks.py b/backend/igny8_core/ai/tasks.py index fc3f1f07..67f03d54 100644 --- a/backend/igny8_core/ai/tasks.py +++ b/backend/igny8_core/ai/tasks.py @@ -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 diff --git a/backend/igny8_core/modules/writer/serializers.py b/backend/igny8_core/modules/writer/serializers.py index dd083893..12e77283 100644 --- a/backend/igny8_core/modules/writer/serializers.py +++ b/backend/igny8_core/modules/writer/serializers.py @@ -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 diff --git a/frontend/src/components/common/ContentImageCell.tsx b/frontend/src/components/common/ContentImageCell.tsx index e2dfdb46..50e79909 100644 --- a/frontend/src/components/common/ContentImageCell.tsx +++ b/frontend/src/components/common/ContentImageCell.tsx @@ -25,6 +25,24 @@ interface ContentImageCellProps { export default function ContentImageCell({ image, maxPromptLength = 100 }: ContentImageCellProps) { const [showFullPrompt, setShowFullPrompt] = useState(false); + // Convert local file path to web-accessible URL + const getLocalImageUrl = (imagePath: string): string => { + // If path contains 'ai-images', convert to web URL + if (imagePath.includes('ai-images')) { + const filename = imagePath.split('ai-images/')[1] || imagePath.split('ai-images\\')[1]; + if (filename) { + return `/images/ai-images/${filename}`; + } + } + // If path is already a web path, return as-is + if (imagePath.startsWith('/images/')) { + return imagePath; + } + // Otherwise, try to extract filename and use ai-images path + const filename = imagePath.split('/').pop() || imagePath.split('\\').pop(); + return `/images/ai-images/${filename}`; + }; + if (!image) { return (
Image not available
-No URL available
+ {image.status === 'generated' && ( +Image not available
+Image not available
+No URL available
+