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:
IGNY8 VPS (Salman)
2025-11-12 04:45:13 +00:00
parent c29ecc1664
commit 03909a1fab
4 changed files with 179 additions and 61 deletions

View File

@@ -729,25 +729,74 @@ class AICore:
body = response.json() body = response.json()
print(f"[AI][{function_name}] Runware response type: {type(body)}, length: {len(body) if isinstance(body, list) else 'N/A'}") 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] # Runware returns array: [auth_result, image_result]
# image_result has 'data' array with image objects containing 'imageURL' # image_result has 'data' array with image objects containing 'imageURL'
# Reference: AIProcessor has more robust parsing - match that logic
image_url = None image_url = None
error_msg = None
if isinstance(body, list): if isinstance(body, list):
# Find the imageInference result (usually second element) # Case 1: Array response - find the imageInference result
for item in body: 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): if isinstance(item, dict):
# Check for 'data' key (image result) # Check if this is the image result with 'data' key
if 'data' in item and isinstance(item['data'], list) and len(item['data']) > 0: if 'data' in item:
image_data = item['data'][0] data = item['data']
image_url = image_data.get('imageURL') or image_data.get('image_url') print(f"[AI][{function_name}] Found 'data' key, type: {type(data)}")
if image_url: if isinstance(data, list) and len(data) > 0:
break first_item = data[0]
# Check for direct 'imageURL' (fallback) print(f"[AI][{function_name}] First data item keys: {list(first_item.keys()) if isinstance(first_item, dict) else 'N/A'}")
elif 'imageURL' in item: image_url = first_item.get('imageURL') or first_item.get('image_url')
image_url = item.get('imageURL') if 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 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: if image_url:
@@ -763,8 +812,10 @@ class AICore:
'error': None, 'error': None,
} }
else: 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}") 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 { return {
'url': None, 'url': None,
'provider': 'runware', 'provider': 'runware',

View File

@@ -537,12 +537,17 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
from pathlib import Path from pathlib import Path
# Create images directory if it doesn't exist # Create images directory if it doesn't exist
# Use /data/app/igny8/images (mounted volume) for persistence # Use frontend/public/images/ai-images/ for web accessibility (like /images/logo/)
# Fallback to /data/app/images if mounted path not available # This allows images to be served via app.igny8.com/images/ai-images/
images_dir = '/data/app/igny8/images' # Use mounted volume path
write_test_passed = False write_test_passed = False
images_dir = None
# Try frontend/public/images/ai-images/ first (web-accessible)
try: 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) os.makedirs(images_dir, exist_ok=True)
# Test write access # Test write access
test_file = os.path.join(images_dir, '.write_test') 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') f.write('test')
os.remove(test_file) os.remove(test_file)
write_test_passed = True write_test_passed = True
logger.info(f"[process_image_generation_queue] Image {image_id} - Directory writable (mounted volume): {images_dir}") logger.info(f"[process_image_generation_queue] Image {image_id} - Directory writable (web-accessible): {images_dir}")
except Exception as write_test_error: except Exception as web_dir_error:
logger.warning(f"[process_image_generation_queue] Image {image_id} - Mounted directory not writable: {images_dir}, error: {write_test_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/images # Fallback to /data/app/igny8/images (mounted volume)
images_dir = '/data/app/images'
try: try:
images_dir = '/data/app/igny8/images'
os.makedirs(images_dir, exist_ok=True) os.makedirs(images_dir, exist_ok=True)
test_file = os.path.join(images_dir, '.write_test') test_file = os.path.join(images_dir, '.write_test')
with open(test_file, 'w') as f: with open(test_file, 'w') as f:
f.write('test') f.write('test')
os.remove(test_file) os.remove(test_file)
write_test_passed = True write_test_passed = True
logger.info(f"[process_image_generation_queue] Image {image_id} - Using fallback directory (writable): {images_dir}") logger.info(f"[process_image_generation_queue] Image {image_id} - Using mounted volume directory: {images_dir}")
except Exception as fallback_error: except Exception as mounted_error:
logger.warning(f"[process_image_generation_queue] Image {image_id} - Fallback directory also not writable: {images_dir}, error: {fallback_error}") logger.warning(f"[process_image_generation_queue] Image {image_id} - Mounted directory not writable: {images_dir}, error: {mounted_error}")
# Final fallback to project-relative path # Final fallback to /data/app/images
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')
try: try:
images_dir = '/data/app/images'
os.makedirs(images_dir, exist_ok=True) os.makedirs(images_dir, exist_ok=True)
test_file = os.path.join(images_dir, '.write_test') test_file = os.path.join(images_dir, '.write_test')
with open(test_file, 'w') as f: with open(test_file, 'w') as f:
f.write('test') f.write('test')
os.remove(test_file) os.remove(test_file)
write_test_passed = True 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: except Exception as final_error:
logger.error(f"[process_image_generation_queue] Image {image_id} - All directories not writable. Last error: {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}") 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: if not write_test_passed:
raise Exception(f"Failed to find writable directory for saving images") 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 import time
timestamp = int(time.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) file_path = os.path.join(images_dir, filename)
# Download image # Download image

View File

@@ -172,10 +172,19 @@ class ContentImageSerializer(serializers.ModelSerializer):
def get_image_url(self, obj): def get_image_url(self, obj):
""" """
Return proper HTTP URL for image. 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: 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') request = self.context.get('request')
if request: if request:
# Build absolute URL for file endpoint # Build absolute URL for file endpoint

View File

@@ -25,6 +25,24 @@ interface ContentImageCellProps {
export default function ContentImageCell({ image, maxPromptLength = 100 }: ContentImageCellProps) { export default function ContentImageCell({ image, maxPromptLength = 100 }: ContentImageCellProps) {
const [showFullPrompt, setShowFullPrompt] = useState(false); 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) { if (!image) {
return ( return (
<div className="text-gray-400 dark:text-gray-500 text-sm">-</div> <div className="text-gray-400 dark:text-gray-500 text-sm">-</div>
@@ -77,34 +95,69 @@ export default function ContentImageCell({ image, maxPromptLength = 100 }: Conte
</div> </div>
)} )}
{image.status === 'generated' && image.image_url && ( {image.status === 'generated' && (
<a <div className="space-y-1">
href={image.image_url} {/* Show local image if available, otherwise show original URL */}
target="_blank" {image.image_path ? (
rel="noopener noreferrer" <>
className="block group" <img
> src={getLocalImageUrl(image.image_path)}
<img alt={prompt || 'Generated image'}
src={image.image_url} className="w-full h-24 object-cover rounded border border-gray-300 dark:border-gray-600"
alt={prompt || 'Generated image'} onError={(e) => {
className="w-full h-24 object-cover rounded border border-gray-300 dark:border-gray-600 group-hover:opacity-80 transition-opacity" // Fallback to original URL if local image fails
onError={(e) => { const target = e.target as HTMLImageElement;
// Fallback to placeholder if image fails to load if (image.image_url) {
const target = e.target as HTMLImageElement; target.src = image.image_url;
target.style.display = 'none'; } else {
target.parentElement!.innerHTML = ` target.style.display = 'none';
<div class="w-full h-24 bg-gray-200 dark:bg-gray-700 rounded border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center"> target.parentElement!.innerHTML = `
<p class="text-xs text-gray-500 dark:text-gray-400">Image not available</p> <div class="w-full h-24 bg-gray-200 dark:bg-gray-700 rounded border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center">
</div> <p class="text-xs text-gray-500 dark:text-gray-400">Image not available</p>
`; </div>
}} `;
/> }
</a> }}
)} />
{image.image_url && (
{image.status === 'generated' && !image.image_url && ( <a
<div className="w-full h-24 bg-yellow-100 dark:bg-yellow-900/20 rounded border border-yellow-300 dark:border-yellow-700 flex items-center justify-center"> href={image.image_url}
<p className="text-xs text-yellow-700 dark:text-yellow-400">No URL available</p> target="_blank"
rel="noopener noreferrer"
className="block w-full text-center px-2 py-1 text-xs text-brand-500 hover:text-brand-600 dark:text-brand-400 dark:hover:text-brand-300 border border-brand-300 dark:border-brand-700 rounded hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
>
View Original
</a>
)}
</>
) : image.image_url ? (
<a
href={image.image_url}
target="_blank"
rel="noopener noreferrer"
className="block group"
>
<img
src={image.image_url}
alt={prompt || 'Generated image'}
className="w-full h-24 object-cover rounded border border-gray-300 dark:border-gray-600 group-hover:opacity-80 transition-opacity"
onError={(e) => {
// Fallback to placeholder if image fails to load
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.parentElement!.innerHTML = `
<div class="w-full h-24 bg-gray-200 dark:bg-gray-700 rounded border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center">
<p class="text-xs text-gray-500 dark:text-gray-400">Image not available</p>
</div>
`;
}}
/>
</a>
) : (
<div className="w-full h-24 bg-yellow-100 dark:bg-yellow-900/20 rounded border border-yellow-300 dark:border-yellow-700 flex items-center justify-center">
<p className="text-xs text-yellow-700 dark:text-yellow-400">No URL available</p>
</div>
)}
</div> </div>
)} )}