some improvements
This commit is contained in:
@@ -176,10 +176,11 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
logger.info(f"[process_image_generation_queue] Image generation settings found. Config keys: {list(config.keys())}")
|
||||
logger.info(f"[process_image_generation_queue] Full config: {config}")
|
||||
|
||||
# FORCE OPENAI DALL-E 2 ONLY FOR NOW
|
||||
provider = 'openai'
|
||||
model = 'dall-e-2'
|
||||
logger.info(f"[process_image_generation_queue] FORCED PROVIDER: {provider}, MODEL: {model} (ignoring config provider: {config.get('provider', 'openai')}, model: {config.get('model', 'dall-e-3')})")
|
||||
# Get provider and model from config (respect user settings)
|
||||
provider = config.get('provider', 'openai')
|
||||
# Get model - try 'model' first, then 'imageModel' as fallback
|
||||
model = config.get('model') or config.get('imageModel') or 'dall-e-3'
|
||||
logger.info(f"[process_image_generation_queue] Using PROVIDER: {provider}, MODEL: {model} from settings")
|
||||
image_type = config.get('image_type', 'realistic')
|
||||
image_format = config.get('image_format', 'webp')
|
||||
desktop_enabled = config.get('desktop_enabled', True)
|
||||
@@ -202,33 +203,32 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
|
||||
# Get provider API key (using same approach as test image generation)
|
||||
# Note: API key is stored as 'apiKey' (camelCase) in IntegrationSettings.config
|
||||
# FORCED: Always use 'openai' for provider (DALL-E 2 only)
|
||||
logger.info(f"[process_image_generation_queue] Step 2: Loading {provider.upper()} API key (FORCED: openai)")
|
||||
logger.info(f"[process_image_generation_queue] Step 2: Loading {provider.upper()} API key")
|
||||
try:
|
||||
provider_settings = IntegrationSettings.objects.get(
|
||||
account=account,
|
||||
integration_type='openai', # FORCED: Always use 'openai' for DALL-E 2
|
||||
integration_type=provider, # Use the provider from settings
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[process_image_generation_queue] OPENAI integration settings found")
|
||||
logger.info(f"[process_image_generation_queue] OPENAI config keys: {list(provider_settings.config.keys()) if provider_settings.config else 'None'}")
|
||||
logger.info(f"[process_image_generation_queue] {provider.upper()} integration settings found")
|
||||
logger.info(f"[process_image_generation_queue] {provider.upper()} config keys: {list(provider_settings.config.keys()) if provider_settings.config else 'None'}")
|
||||
|
||||
api_key = provider_settings.config.get('apiKey') if provider_settings.config else None
|
||||
if not api_key:
|
||||
logger.error(f"[process_image_generation_queue] OPENAI API key not found in config")
|
||||
logger.error(f"[process_image_generation_queue] OPENAI config: {provider_settings.config}")
|
||||
return {'success': False, 'error': 'OPENAI API key not configured'}
|
||||
logger.error(f"[process_image_generation_queue] {provider.upper()} API key not found in config")
|
||||
logger.error(f"[process_image_generation_queue] {provider.upper()} config: {provider_settings.config}")
|
||||
return {'success': False, 'error': f'{provider.upper()} API key not configured'}
|
||||
|
||||
# Log API key presence (but not the actual key for security)
|
||||
api_key_preview = f"{api_key[:10]}...{api_key[-4:]}" if len(api_key) > 14 else "***"
|
||||
logger.info(f"[process_image_generation_queue] OPENAI API key retrieved successfully (length: {len(api_key)}, preview: {api_key_preview})")
|
||||
logger.info(f"[process_image_generation_queue] {provider.upper()} API key retrieved successfully (length: {len(api_key)}, preview: {api_key_preview})")
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.error(f"[process_image_generation_queue] ERROR: OPENAI integration settings not found")
|
||||
logger.error(f"[process_image_generation_queue] Account: {account.id if account else 'None'}, integration_type: 'openai'")
|
||||
return {'success': False, 'error': 'OPENAI integration not found or not active'}
|
||||
logger.error(f"[process_image_generation_queue] ERROR: {provider.upper()} integration settings not found")
|
||||
logger.error(f"[process_image_generation_queue] Account: {account.id if account else 'None'}, integration_type: '{provider}'")
|
||||
return {'success': False, 'error': f'{provider.upper()} integration not found or not active'}
|
||||
except Exception as e:
|
||||
logger.error(f"[process_image_generation_queue] ERROR getting OPENAI API key: {e}", exc_info=True)
|
||||
return {'success': False, 'error': f'Error retrieving OPENAI API key: {str(e)}'}
|
||||
logger.error(f"[process_image_generation_queue] ERROR getting {provider.upper()} API key: {e}", exc_info=True)
|
||||
return {'success': False, 'error': f'Error retrieving {provider.upper()} API key: {str(e)}'}
|
||||
|
||||
# Get image prompt template (has placeholders: {image_type}, {post_title}, {image_prompt})
|
||||
try:
|
||||
@@ -599,8 +599,14 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
|
||||
# Verify file was actually saved and exists
|
||||
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
|
||||
saved_file_path = file_path
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Saved to: {file_path} ({len(response.content)} bytes)")
|
||||
logger.info(f"[process_image_generation_queue] Image {image_id} - Saved to: {file_path} ({len(response.content)} bytes, verified: {os.path.getsize(file_path)} bytes on disk)")
|
||||
else:
|
||||
logger.error(f"[process_image_generation_queue] Image {image_id} - File write appeared to succeed but file not found or empty: {file_path}")
|
||||
saved_file_path = None
|
||||
raise Exception(f"File was not saved successfully to {file_path}")
|
||||
|
||||
except Exception as download_error:
|
||||
logger.error(f"[process_image_generation_queue] Image {image_id} - Failed to download/save image: {download_error}", exc_info=True)
|
||||
|
||||
@@ -153,6 +153,8 @@ class ImagesSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ContentImageSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for individual image in grouped content images"""
|
||||
image_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Images
|
||||
fields = [
|
||||
@@ -167,6 +169,24 @@ class ContentImageSerializer(serializers.ModelSerializer):
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
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).
|
||||
"""
|
||||
if obj.image_path:
|
||||
# Return file endpoint URL for locally saved images
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
# Build absolute URL for file endpoint
|
||||
file_url = request.build_absolute_uri(f'/api/v1/writer/images/{obj.id}/file/')
|
||||
return file_url
|
||||
else:
|
||||
# Fallback: return relative URL if no request context
|
||||
return f'/api/v1/writer/images/{obj.id}/file/'
|
||||
# Fallback to original image_url (API URL) if no local path
|
||||
return obj.image_url
|
||||
|
||||
|
||||
class ContentImagesGroupSerializer(serializers.Serializer):
|
||||
"""Serializer for grouped content images - one row per content"""
|
||||
|
||||
@@ -395,10 +395,46 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
|
||||
file_path = image.image_path
|
||||
|
||||
# Verify file exists
|
||||
# Verify file exists - if not, try alternative locations
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"[serve_image_file] Image {pk} - File not found at saved path: {file_path}, trying alternative locations...")
|
||||
|
||||
# Try alternative locations based on the filename
|
||||
filename = os.path.basename(file_path)
|
||||
alternative_paths = [
|
||||
'/data/app/igny8/images/' + filename, # Primary location
|
||||
'/data/app/images/' + filename, # Fallback location
|
||||
]
|
||||
|
||||
# Also try project-relative path
|
||||
try:
|
||||
from django.conf import settings
|
||||
from pathlib import Path
|
||||
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path(__file__).resolve().parent.parent.parent
|
||||
alternative_paths.append(str(base_dir / 'data' / 'app' / 'images' / filename))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try each alternative path
|
||||
found = False
|
||||
for alt_path in alternative_paths:
|
||||
if os.path.exists(alt_path):
|
||||
file_path = alt_path
|
||||
logger.info(f"[serve_image_file] Image {pk} - Found file at alternative location: {file_path}")
|
||||
# Update database with correct path
|
||||
try:
|
||||
image.image_path = file_path
|
||||
image.save(update_fields=['image_path'])
|
||||
logger.info(f"[serve_image_file] Image {pk} - Updated database with correct path: {file_path}")
|
||||
except Exception as update_error:
|
||||
logger.warning(f"[serve_image_file] Image {pk} - Failed to update database path: {update_error}")
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
logger.error(f"[serve_image_file] Image {pk} - File not found in any location. Tried: {[file_path] + alternative_paths}")
|
||||
return Response({
|
||||
'error': f'Image file not found at: {file_path}'
|
||||
'error': f'Image file not found at: {file_path} (also checked alternative locations)'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Check if file is readable
|
||||
@@ -603,11 +639,15 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
else:
|
||||
overall_status = 'pending'
|
||||
|
||||
# Create serializer instances with request context for proper URL generation
|
||||
featured_serializer = ContentImageSerializer(featured_image, context={'request': request}) if featured_image else None
|
||||
in_article_serializers = [ContentImageSerializer(img, context={'request': request}) for img in in_article_images]
|
||||
|
||||
grouped_data.append({
|
||||
'content_id': content.id,
|
||||
'content_title': content.title or content.meta_title or f"Content #{content.id}",
|
||||
'featured_image': ContentImageSerializer(featured_image).data if featured_image else None,
|
||||
'in_article_images': [ContentImageSerializer(img).data for img in in_article_images],
|
||||
'featured_image': featured_serializer.data if featured_serializer else None,
|
||||
'in_article_images': [s.data for s in in_article_serializers],
|
||||
'overall_status': overall_status,
|
||||
})
|
||||
except Content.DoesNotExist:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Stage 1: Shows all progress bars immediately when Generate button is clicked
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Modal } from '../ui/modal';
|
||||
import { FileIcon, TimeIcon, CheckCircleIcon, ErrorIcon } from '../../icons';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
@@ -53,6 +53,9 @@ export default function ImageQueueModal({
|
||||
onLog,
|
||||
}: ImageQueueModalProps) {
|
||||
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
|
||||
// Track smooth progress animation for each item
|
||||
const [smoothProgress, setSmoothProgress] = useState<Record<number, number>>({});
|
||||
const progressIntervalsRef = useRef<Record<number, NodeJS.Timeout>>({});
|
||||
|
||||
useEffect(() => {
|
||||
setLocalQueue(queue);
|
||||
@@ -64,6 +67,77 @@ export default function ImageQueueModal({
|
||||
}
|
||||
}, [localQueue, onUpdateQueue]);
|
||||
|
||||
// Smooth progress animation (like reference plugin)
|
||||
useEffect(() => {
|
||||
// Start smooth progress for items that are processing
|
||||
localQueue.forEach((item) => {
|
||||
if (item.status === 'processing' && item.progress < 95) {
|
||||
// Only start animation if not already running
|
||||
if (!progressIntervalsRef.current[item.index]) {
|
||||
// Start from current progress or 0
|
||||
let currentProgress = smoothProgress[item.index] || item.progress || 0;
|
||||
let phase = currentProgress < 50 ? 1 : currentProgress < 75 ? 2 : 3;
|
||||
let phaseStartTime = Date.now();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Date.now() - phaseStartTime;
|
||||
|
||||
if (phase === 1 && currentProgress < 50) {
|
||||
// Phase 1: 0% to 50% in 7 seconds (7.14% per second)
|
||||
currentProgress += 0.714;
|
||||
if (currentProgress >= 50 || elapsed >= 7000) {
|
||||
currentProgress = 50;
|
||||
phase = 2;
|
||||
phaseStartTime = Date.now();
|
||||
}
|
||||
} else if (phase === 2 && currentProgress < 75) {
|
||||
// Phase 2: 50% to 75% in 5 seconds (5% per second)
|
||||
currentProgress += 0.5;
|
||||
if (currentProgress >= 75 || elapsed >= 5000) {
|
||||
currentProgress = 75;
|
||||
phase = 3;
|
||||
phaseStartTime = Date.now();
|
||||
}
|
||||
} else if (phase === 3 && currentProgress < 95) {
|
||||
// Phase 3: 75% to 95% - 5% every second
|
||||
if (elapsed >= 1000) {
|
||||
currentProgress = Math.min(95, currentProgress + 5);
|
||||
phaseStartTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
setSmoothProgress(prev => ({
|
||||
...prev,
|
||||
[item.index]: Math.round(currentProgress)
|
||||
}));
|
||||
}, 100);
|
||||
|
||||
progressIntervalsRef.current[item.index] = interval;
|
||||
}
|
||||
} else {
|
||||
// Stop animation if item is no longer processing
|
||||
if (progressIntervalsRef.current[item.index]) {
|
||||
clearInterval(progressIntervalsRef.current[item.index]);
|
||||
delete progressIntervalsRef.current[item.index];
|
||||
}
|
||||
// Clear smooth progress for completed/failed items
|
||||
if (item.status === 'completed' || item.status === 'failed') {
|
||||
setSmoothProgress(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[item.index];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
Object.values(progressIntervalsRef.current).forEach(interval => clearInterval(interval));
|
||||
progressIntervalsRef.current = {};
|
||||
};
|
||||
}, [localQueue, smoothProgress]);
|
||||
|
||||
// Polling for task status updates
|
||||
useEffect(() => {
|
||||
if (!isOpen || !taskId) return;
|
||||
@@ -190,6 +264,14 @@ export default function ImageQueueModal({
|
||||
const result = results?.find((r: any) => r.image_id === item.imageId);
|
||||
|
||||
if (result) {
|
||||
// Stop smooth animation for completed/failed items
|
||||
if (result.status === 'completed' || result.status === 'failed') {
|
||||
if (progressIntervalsRef.current[item.index]) {
|
||||
clearInterval(progressIntervalsRef.current[item.index]);
|
||||
delete progressIntervalsRef.current[item.index];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
status: result.status === 'completed' ? 'completed' :
|
||||
@@ -197,23 +279,30 @@ export default function ImageQueueModal({
|
||||
progress: result.status === 'completed' ? 100 :
|
||||
result.status === 'failed' ? 0 :
|
||||
// Use current_image_progress if this is the current image being processed
|
||||
// Otherwise use smooth progress animation
|
||||
(current_image_id === item.imageId && current_image_progress !== undefined) ? current_image_progress :
|
||||
index + 1 < current_image ? 100 :
|
||||
index + 1 === current_image ? 0 : 0,
|
||||
imageUrl: result.image_url || item.imageUrl,
|
||||
index + 1 === current_image ? (smoothProgress[item.index] || 0) : 0,
|
||||
imageUrl: result.image_path
|
||||
? `/api/v1/writer/images/${item.imageId}/file/`
|
||||
: (result.image_url || item.imageUrl),
|
||||
error: result.error || null
|
||||
};
|
||||
}
|
||||
|
||||
// Update based on current_image index and progress
|
||||
if (index + 1 < current_image) {
|
||||
// Already completed
|
||||
// Already completed - stop animation
|
||||
if (progressIntervalsRef.current[item.index]) {
|
||||
clearInterval(progressIntervalsRef.current[item.index]);
|
||||
delete progressIntervalsRef.current[item.index];
|
||||
}
|
||||
return { ...item, status: 'completed', progress: 100 };
|
||||
} else if (index + 1 === current_image || current_image_id === item.imageId) {
|
||||
// Currently processing - use current_image_progress if available
|
||||
// Currently processing - use current_image_progress if available, otherwise smooth progress
|
||||
const progress = (current_image_progress !== undefined && current_image_id === item.imageId)
|
||||
? current_image_progress
|
||||
: 0;
|
||||
: (smoothProgress[item.index] || 0);
|
||||
return { ...item, status: 'processing', progress };
|
||||
}
|
||||
|
||||
@@ -230,11 +319,19 @@ export default function ImageQueueModal({
|
||||
const taskResult = results?.find((r: any) => r.image_id === item.imageId);
|
||||
|
||||
if (taskResult) {
|
||||
// Stop smooth animation
|
||||
if (progressIntervalsRef.current[item.index]) {
|
||||
clearInterval(progressIntervalsRef.current[item.index]);
|
||||
delete progressIntervalsRef.current[item.index];
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
status: taskResult.status === 'completed' ? 'completed' : 'failed',
|
||||
progress: taskResult.status === 'completed' ? 100 : 0,
|
||||
imageUrl: taskResult.image_url || item.imageUrl,
|
||||
imageUrl: taskResult.image_path
|
||||
? `/api/v1/writer/images/${item.imageId}/file/`
|
||||
: (taskResult.image_url || item.imageUrl),
|
||||
error: taskResult.error || null
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user