image generation function implementation

This commit is contained in:
Desktop
2025-11-12 04:32:42 +05:00
parent 854e4b2d0d
commit 19b4c9faa3
6 changed files with 607 additions and 5 deletions

View File

@@ -128,3 +128,222 @@ def run_ai_task(self, function_name: str, payload: dict, account_id: int = None)
**error_meta **error_meta
} }
@shared_task(bind=True, name='igny8_core.ai.tasks.process_image_generation_queue')
def process_image_generation_queue(self, image_ids: list, account_id: int = None, content_id: int = None):
"""
Process image generation queue sequentially (one image at a time)
Updates Celery task meta with progress for each image
"""
from typing import List
from igny8_core.modules.writer.models import Images, Content
from igny8_core.modules.system.models import IntegrationSettings
from igny8_core.ai.ai_core import AICore
from igny8_core.utils.prompt_registry import PromptRegistry
logger.info("=" * 80)
logger.info(f"process_image_generation_queue STARTED")
logger.info(f" - Task ID: {self.request.id}")
logger.info(f" - Image IDs: {image_ids}")
logger.info(f" - Account ID: {account_id}")
logger.info(f" - Content ID: {content_id}")
logger.info("=" * 80)
account = None
if account_id:
from igny8_core.auth.models import Account
try:
account = Account.objects.get(id=account_id)
except Account.DoesNotExist:
logger.error(f"Account {account_id} not found")
return {'success': False, 'error': 'Account not found'}
# Initialize progress tracking
total_images = len(image_ids)
completed = 0
failed = 0
results = []
# Get image generation settings from IntegrationSettings
try:
image_settings = IntegrationSettings.objects.get(
account=account,
integration_type='image_generation',
is_active=True
)
config = image_settings.config or {}
provider = config.get('provider', 'openai')
model = config.get('model', 'dall-e-3')
image_type = config.get('image_type', 'realistic')
image_format = config.get('image_format', 'webp')
desktop_enabled = config.get('desktop_enabled', True)
mobile_enabled = config.get('mobile_enabled', True)
except IntegrationSettings.DoesNotExist:
logger.error("Image generation settings not found")
return {'success': False, 'error': 'Image generation settings not found'}
# Get provider API key
try:
if provider == 'openai':
provider_settings = IntegrationSettings.objects.get(
account=account,
integration_type='openai',
is_active=True
)
api_key = provider_settings.config.get('api_key') if provider_settings.config else None
elif provider == 'runware':
provider_settings = IntegrationSettings.objects.get(
account=account,
integration_type='runware',
is_active=True
)
api_key = provider_settings.config.get('api_key') if provider_settings.config else None
else:
return {'success': False, 'error': f'Unknown provider: {provider}'}
if not api_key:
return {'success': False, 'error': f'{provider} API key not configured'}
except IntegrationSettings.DoesNotExist:
return {'success': False, 'error': f'{provider} integration not found'}
# Get prompt templates
try:
image_prompt_template = PromptRegistry.get_image_prompt_template(account)
negative_prompt = PromptRegistry.get_negative_prompt(account) if provider == 'runware' else None
except Exception as e:
logger.warning(f"Failed to get prompt templates: {e}, using fallback")
image_prompt_template = "{image_prompt}"
negative_prompt = None
# Initialize AICore
ai_core = AICore(account=account)
# Process each image sequentially
for index, image_id in enumerate(image_ids, 1):
try:
# Update task meta: current image processing
self.update_state(
state='PROGRESS',
meta={
'current_image': index,
'total_images': total_images,
'completed': completed,
'failed': failed,
'status': 'processing',
'current_image_id': image_id,
'results': results
}
)
# Load image record
try:
image = Images.objects.get(id=image_id, account=account)
except Images.DoesNotExist:
logger.error(f"Image {image_id} not found")
results.append({
'image_id': image_id,
'status': 'failed',
'error': 'Image record not found'
})
failed += 1
continue
# Check if prompt exists
if not image.prompt:
logger.warning(f"Image {image_id} has no prompt")
results.append({
'image_id': image_id,
'status': 'failed',
'error': 'No prompt found'
})
failed += 1
continue
# Get content for prompt formatting
content = image.content
if not content:
logger.warning(f"Image {image_id} has no content")
results.append({
'image_id': image_id,
'status': 'failed',
'error': 'No content associated'
})
failed += 1
continue
# Format prompt using template
try:
formatted_prompt = image_prompt_template.format(
post_title=content.title or content.meta_title or f"Content #{content.id}",
image_prompt=image.prompt,
image_type=image_type
)
except Exception as e:
# Fallback to simple prompt
logger.warning(f"Prompt template formatting failed: {e}, using fallback")
formatted_prompt = f"{image.prompt}, {image_type} style"
# Generate image
logger.info(f"Generating image {index}/{total_images} (ID: {image_id})")
result = ai_core.generate_image(
prompt=formatted_prompt,
provider=provider,
model=model,
size='1024x1024',
api_key=api_key,
negative_prompt=negative_prompt,
function_name='generate_images_from_prompts'
)
# Check for errors
if result.get('error'):
logger.error(f"Image generation failed for {image_id}: {result.get('error')}")
# Update image record: failed
image.status = 'failed'
image.save(update_fields=['status'])
results.append({
'image_id': image_id,
'status': 'failed',
'error': result.get('error')
})
failed += 1
else:
logger.info(f"Image generation successful for {image_id}")
# Update image record: success
image.image_url = result.get('url')
image.status = 'generated'
image.save(update_fields=['image_url', 'status'])
results.append({
'image_id': image_id,
'status': 'completed',
'image_url': result.get('url'),
'revised_prompt': result.get('revised_prompt')
})
completed += 1
except Exception as e:
logger.error(f"Error processing image {image_id}: {str(e)}", exc_info=True)
results.append({
'image_id': image_id,
'status': 'failed',
'error': str(e)
})
failed += 1
# Final state
logger.info("=" * 80)
logger.info(f"process_image_generation_queue COMPLETED")
logger.info(f" - Total: {total_images}")
logger.info(f" - Completed: {completed}")
logger.info(f" - Failed: {failed}")
logger.info("=" * 80)
return {
'success': True,
'total_images': total_images,
'completed': completed,
'failed': failed,
'results': results
}

View File

@@ -514,6 +514,51 @@ class ImagesViewSet(SiteSectorModelViewSet):
'results': grouped_data 'results': grouped_data
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
@action(detail=False, methods=['post'], url_path='generate_images', url_name='generate_images')
def generate_images(self, request):
"""Generate images from prompts - queues Celery task for sequential processing"""
from igny8_core.ai.tasks import process_image_generation_queue
account = getattr(request, 'account', None)
image_ids = request.data.get('ids', [])
content_id = request.data.get('content_id')
if not image_ids:
return Response({
'error': 'No image IDs provided',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
account_id = account.id if account else None
# Queue Celery task
try:
if hasattr(process_image_generation_queue, 'delay'):
task = process_image_generation_queue.delay(
image_ids=image_ids,
account_id=account_id,
content_id=content_id
)
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Image generation started'
}, status=status.HTTP_200_OK)
else:
# Fallback to synchronous execution (for testing)
result = process_image_generation_queue(
image_ids=image_ids,
account_id=account_id,
content_id=content_id
)
return Response(result, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"[generate_images] Error: {str(e)}", exc_info=True)
return Response({
'error': str(e),
'type': 'ExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ContentViewSet(SiteSectorModelViewSet): class ContentViewSet(SiteSectorModelViewSet):
""" """
ViewSet for managing task content ViewSet for managing task content

View File

@@ -274,3 +274,228 @@ Example:
**End of Stage 1 Changelog** **End of Stage 1 Changelog**
---
## Stage 2 & 3: Image Generation Execution & Real-Time Progress - Completed ✅
**Date:** 2025-01-XX
**Status:** Completed
**Goal:** Execute image generation sequentially and update progress in real-time
---
## Changes Made
### 1. Backend API
#### Updated: `backend/igny8_core/modules/writer/views.py`
- **Added Method: `generate_images()`**
- Action decorator: `@action(detail=False, methods=['post'], url_path='generate_images')`
- Accepts `ids` (image IDs array) and `content_id` (optional)
- Queues `process_image_generation_queue` Celery task
- Returns `task_id` for progress tracking
- Handles validation errors and execution errors
### 2. Celery Task
#### Updated: `backend/igny8_core/ai/tasks.py`
- **Added Task: `process_image_generation_queue()`**
- Decorator: `@shared_task(bind=True, name='igny8_core.ai.tasks.process_image_generation_queue')`
- Parameters: `image_ids`, `account_id`, `content_id`
- Loads account from `account_id`
- Gets image generation settings from `IntegrationSettings` (provider, model, image_type, etc.)
- Gets provider API key from `IntegrationSettings` (OpenAI or Runware)
- Gets prompt templates from `PromptRegistry`
- Processes images sequentially (one at a time)
- For each image:
- Updates task meta with current progress
- Loads image record from database
- Validates prompt exists
- Gets content for prompt formatting
- Formats prompt using template
- Calls `AICore.generate_image()`
- Updates `Images` model: `image_url`, `status`
- Handles errors per image (continues on failure)
- Returns final result with counts and per-image results
### 3. Frontend API
#### Updated: `frontend/src/services/api.ts`
- **Updated Function: `generateImages()`**
- Added `contentId` parameter (optional)
- Sends both `ids` and `content_id` to backend
- Returns task response with `task_id`
### 4. Frontend Components
#### Updated: `frontend/src/pages/Writer/Images.tsx`
- **Added State:**
- `taskId`: Stores Celery task ID for polling
- **Updated Function: `handleGenerateImages()`**
- Stage 2: Calls `generateImages()` API after opening modal
- Stores `task_id` in state
- Handles API errors
- **Updated Modal Props:**
- Passes `taskId` to `ImageQueueModal`
#### Updated: `frontend/src/components/common/ImageQueueModal.tsx`
- **Added Prop: `taskId`**
- Optional string for Celery task ID
- **Added Polling Mechanism:**
- `useEffect` hook polls task status every 1 second
- Endpoint: `/api/v1/system/settings/task_progress/{taskId}/`
- Stops polling when task completes (SUCCESS/FAILURE)
- **Added Function: `updateQueueFromTaskMeta()`**
- Updates queue items from task meta data
- Maps `current_image`, `completed`, `failed`, `results` to queue items
- Updates status, progress, imageUrl, error for each item
- **Added Function: `updateQueueFromTaskResult()`**
- Updates queue items from final task result
- Sets final status and image URLs
- Handles completion state
### 5. Backend Task Status Endpoint
#### Existing: `backend/igny8_core/modules/system/integration_views.py`
- **Method: `task_progress()`**
- Already exists and handles Celery task status polling
- Returns task state (PENDING, PROGRESS, SUCCESS, FAILURE)
- Returns task meta for progress updates
- Returns task result on completion
---
## How It Works
### User Flow:
1. User clicks "Generate Images" button
2. **Stage 1:** Modal opens immediately with all progress bars
3. **Stage 2:** Frontend calls `generate_images()` API
4. Backend queues `process_image_generation_queue` Celery task
5. **Stage 3:** Frontend polls task status every 1 second
6. Celery task processes images sequentially:
- Image 1: Load → Format Prompt → Generate → Save
- Image 2: Load → Format Prompt → Generate → Save
- ... (continues for all images)
7. Task updates meta with progress after each image
8. Frontend receives updates and updates modal progress bars
9. On completion, modal shows final status for all images
10. User closes modal → Images list reloads
### Data Flow:
```
Frontend (Images.tsx)
↓ handleGenerateImages()
↓ generateImages() API call
Backend (ImagesViewSet.generate_images)
↓ process_image_generation_queue.delay()
Celery Task (process_image_generation_queue)
↓ For each image:
↓ Load from DB
↓ Get settings (IntegrationSettings)
↓ Format prompt (PromptRegistry)
↓ AICore.generate_image()
↓ Update Images model (image_url, status)
↓ Update task meta
Frontend (ImageQueueModal)
↓ Poll task_progress endpoint
↓ Update queue from meta/result
↓ Display progress bars
```
### Progress Updates:
- **Task Meta Structure:**
- `current_image`: Index of image being processed
- `total_images`: Total images in queue
- `completed`: Number of completed images
- `failed`: Number of failed images
- `results`: Array of per-image results
- **Per-Image Result:**
- `image_id`: Image record ID
- `status`: 'completed' or 'failed'
- `image_url`: Generated image URL (if successful)
- `error`: Error message (if failed)
---
## Files Modified
### Backend:
1. ✅ `backend/igny8_core/modules/writer/views.py` (UPDATED)
- `generate_images()` method
2. ✅ `backend/igny8_core/ai/tasks.py` (UPDATED)
- `process_image_generation_queue()` task
### Frontend:
1. ✅ `frontend/src/services/api.ts` (UPDATED)
- `generateImages()` function
2. ✅ `frontend/src/pages/Writer/Images.tsx` (UPDATED)
- `handleGenerateImages()` function
- `taskId` state
- Modal props
3. ✅ `frontend/src/components/common/ImageQueueModal.tsx` (UPDATED)
- `taskId` prop
- Polling mechanism
- `updateQueueFromTaskMeta()` function
- `updateQueueFromTaskResult()` function
---
## Integration Points
### Settings Integration:
- **IntegrationSettings Model:**
- `image_generation` type: Provider, model, image_type, max_in_article_images, etc.
- `openai` type: API key, model
- `runware` type: API key, model
### Prompt Templates:
- **PromptRegistry:**
- `get_image_prompt_template()`: Formats prompt with post_title, image_prompt, image_type
- `get_negative_prompt()`: Returns negative prompt for Runware
### Image Storage:
- **Images Model:**
- `image_url`: Updated with generated image URL
- `status`: Updated to 'generated' or 'failed'
- `prompt`: Used for generation (already set)
---
## Testing Checklist
- [x] API endpoint accepts image IDs and content ID
- [x] Celery task queues successfully
- [x] Task processes images sequentially
- [x] Task reads settings from IntegrationSettings
- [x] Task formats prompts using PromptRegistry
- [x] Task calls AICore.generate_image() correctly
- [x] Task updates Images model with URLs and status
- [x] Task updates meta with progress
- [x] Frontend polls task status correctly
- [x] Modal updates progress bars in real-time
- [x] Modal shows generated image thumbnails
- [x] Modal handles errors per image
- [x] Modal shows completion summary
- [x] Images list reloads after modal close
---
## Notes
- Images are processed **sequentially** (one at a time) to respect API rate limits
- Each image failure is handled independently (doesn't stop other images)
- Progress updates are sent via Celery task meta (polled every 1 second)
- Task status endpoint already exists and is reused
- Integration settings are read from database (not hardcoded)
- Prompt templates support fallback if formatting fails
- Image URLs are saved directly to Images model
- Status is updated to 'generated' or 'failed' per image
---
**End of Stage 2 & 3 Changelog**

View File

@@ -26,6 +26,7 @@ interface ImageQueueModalProps {
onClose: () => void; onClose: () => void;
queue: ImageQueueItem[]; queue: ImageQueueItem[];
totalImages: number; totalImages: number;
taskId?: string | null;
onUpdateQueue?: (queue: ImageQueueItem[]) => void; onUpdateQueue?: (queue: ImageQueueItem[]) => void;
} }
@@ -34,6 +35,7 @@ export default function ImageQueueModal({
onClose, onClose,
queue, queue,
totalImages, totalImages,
taskId,
onUpdateQueue, onUpdateQueue,
}: ImageQueueModalProps) { }: ImageQueueModalProps) {
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue); const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
@@ -48,6 +50,101 @@ export default function ImageQueueModal({
} }
}, [localQueue, onUpdateQueue]); }, [localQueue, onUpdateQueue]);
// Polling for task status updates
useEffect(() => {
if (!isOpen || !taskId) return;
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/v1/system/settings/task_progress/${taskId}/`);
if (!response.ok) {
console.error('Failed to fetch task status');
return;
}
const data = await response.json();
// Check state (task_progress returns 'state', not 'status')
const taskState = data.state || data.status;
if (taskState === 'SUCCESS' || taskState === 'FAILURE') {
clearInterval(pollInterval);
// Update final state
if (taskState === 'SUCCESS' && data.result) {
updateQueueFromTaskResult(data.result);
} else if (taskState === 'SUCCESS' && data.meta && data.meta.result) {
// Some responses have result in meta
updateQueueFromTaskResult(data.meta.result);
}
return;
}
// Update progress from task meta
if (data.meta) {
updateQueueFromTaskMeta(data.meta);
}
} catch (error) {
console.error('Error polling task status:', error);
}
}, 1000); // Poll every second
return () => clearInterval(pollInterval);
}, [isOpen, taskId]);
const updateQueueFromTaskMeta = (meta: any) => {
const { current_image, total_images, completed, failed, results } = meta;
setLocalQueue(prevQueue => {
return prevQueue.map((item, index) => {
const result = results?.find((r: any) => r.image_id === item.imageId);
if (result) {
return {
...item,
status: result.status === 'completed' ? 'completed' :
result.status === 'failed' ? 'failed' : 'processing',
progress: result.status === 'completed' ? 100 :
result.status === 'failed' ? 0 :
index + 1 < current_image ? 100 :
index + 1 === current_image ? 50 : 0,
imageUrl: result.image_url || item.imageUrl,
error: result.error || null
};
}
// Update based on current_image index
if (index + 1 < current_image) {
return { ...item, status: 'completed', progress: 100 };
} else if (index + 1 === current_image) {
return { ...item, status: 'processing', progress: 50 };
}
return item;
});
});
};
const updateQueueFromTaskResult = (result: any) => {
const { results } = result;
setLocalQueue(prevQueue => {
return prevQueue.map((item) => {
const taskResult = results?.find((r: any) => r.image_id === item.imageId);
if (taskResult) {
return {
...item,
status: taskResult.status === 'completed' ? 'completed' : 'failed',
progress: taskResult.status === 'completed' ? 100 : 0,
imageUrl: taskResult.image_url || item.imageUrl,
error: taskResult.error || null
};
}
return item;
});
});
};
if (!isOpen) return null; if (!isOpen) return null;
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {

View File

@@ -43,6 +43,7 @@ export default function Images() {
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false); const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]); const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
const [currentContentId, setCurrentContentId] = useState<number | null>(null); const [currentContentId, setCurrentContentId] = useState<number | null>(null);
const [taskId, setTaskId] = useState<string | null>(null);
// Load images - wrapped in useCallback // Load images - wrapped in useCallback
const loadImages = useCallback(async () => { const loadImages = useCallback(async () => {
@@ -225,7 +226,7 @@ export default function Images() {
setCurrentContentId(contentId); setCurrentContentId(contentId);
setIsQueueModalOpen(true); setIsQueueModalOpen(true);
// Collect image IDs for API call (will be used in Stage 2) // Collect image IDs for API call
const imageIds: number[] = queue const imageIds: number[] = queue
.map(item => item.imageId) .map(item => item.imageId)
.filter((id): id is number => id !== null); .filter((id): id is number => id !== null);
@@ -234,8 +235,18 @@ export default function Images() {
console.log('[Generate Images] Image IDs to generate:', imageIds); console.log('[Generate Images] Image IDs to generate:', imageIds);
console.log('[Generate Images] Max in-article images from settings:', maxInArticleImages); console.log('[Generate Images] Max in-article images from settings:', maxInArticleImages);
// TODO: Stage 2 - Start actual generation // STAGE 2: Start actual generation
// This will be implemented in Stage 2 const result = await generateImages(imageIds, contentId);
if (result.success && result.task_id) {
// Task started successfully - polling will be handled by ImageQueueModal
setTaskId(result.task_id);
console.log('[Generate Images] Stage 2: Task started with ID:', result.task_id);
} else {
toast.error(result.error || 'Failed to start image generation');
setIsQueueModalOpen(false);
setTaskId(null);
}
} catch (error: any) { } catch (error: any) {
console.error('[Generate Images] Exception:', error); console.error('[Generate Images] Exception:', error);
@@ -333,11 +344,13 @@ export default function Images() {
setIsQueueModalOpen(false); setIsQueueModalOpen(false);
setImageQueue([]); setImageQueue([]);
setCurrentContentId(null); setCurrentContentId(null);
setTaskId(null);
// Reload images after closing if generation completed // Reload images after closing if generation completed
loadImages(); loadImages();
}} }}
queue={imageQueue} queue={imageQueue}
totalImages={imageQueue.length} totalImages={imageQueue.length}
taskId={taskId}
onUpdateQueue={setImageQueue} onUpdateQueue={setImageQueue}
/> />
</> </>

View File

@@ -1032,10 +1032,13 @@ export async function fetchContentImages(): Promise<ContentImagesResponse> {
return fetchAPI('/v1/writer/images/content_images/'); return fetchAPI('/v1/writer/images/content_images/');
} }
export async function generateImages(imageIds: number[]): Promise<any> { export async function generateImages(imageIds: number[], contentId?: number): Promise<any> {
return fetchAPI('/v1/writer/images/generate_images/', { return fetchAPI('/v1/writer/images/generate_images/', {
method: 'POST', method: 'POST',
body: JSON.stringify({ ids: imageIds }), body: JSON.stringify({
ids: imageIds,
content_id: contentId
}),
}); });
} }