image generation function implementation
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user