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
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
}, 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):
|
||||
"""
|
||||
ViewSet for managing task content
|
||||
|
||||
@@ -274,3 +274,228 @@ Example:
|
||||
|
||||
**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;
|
||||
queue: ImageQueueItem[];
|
||||
totalImages: number;
|
||||
taskId?: string | null;
|
||||
onUpdateQueue?: (queue: ImageQueueItem[]) => void;
|
||||
}
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function ImageQueueModal({
|
||||
onClose,
|
||||
queue,
|
||||
totalImages,
|
||||
taskId,
|
||||
onUpdateQueue,
|
||||
}: ImageQueueModalProps) {
|
||||
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
|
||||
@@ -48,6 +50,101 @@ export default function ImageQueueModal({
|
||||
}
|
||||
}, [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;
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
|
||||
@@ -43,6 +43,7 @@ export default function Images() {
|
||||
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
|
||||
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
|
||||
const [currentContentId, setCurrentContentId] = useState<number | null>(null);
|
||||
const [taskId, setTaskId] = useState<string | null>(null);
|
||||
|
||||
// Load images - wrapped in useCallback
|
||||
const loadImages = useCallback(async () => {
|
||||
@@ -225,7 +226,7 @@ export default function Images() {
|
||||
setCurrentContentId(contentId);
|
||||
setIsQueueModalOpen(true);
|
||||
|
||||
// Collect image IDs for API call (will be used in Stage 2)
|
||||
// Collect image IDs for API call
|
||||
const imageIds: number[] = queue
|
||||
.map(item => item.imageId)
|
||||
.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] Max in-article images from settings:', maxInArticleImages);
|
||||
|
||||
// TODO: Stage 2 - Start actual generation
|
||||
// This will be implemented in Stage 2
|
||||
// STAGE 2: Start actual generation
|
||||
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) {
|
||||
console.error('[Generate Images] Exception:', error);
|
||||
@@ -333,11 +344,13 @@ export default function Images() {
|
||||
setIsQueueModalOpen(false);
|
||||
setImageQueue([]);
|
||||
setCurrentContentId(null);
|
||||
setTaskId(null);
|
||||
// Reload images after closing if generation completed
|
||||
loadImages();
|
||||
}}
|
||||
queue={imageQueue}
|
||||
totalImages={imageQueue.length}
|
||||
taskId={taskId}
|
||||
onUpdateQueue={setImageQueue}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1032,10 +1032,13 @@ export async function fetchContentImages(): Promise<ContentImagesResponse> {
|
||||
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/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: imageIds }),
|
||||
body: JSON.stringify({
|
||||
ids: imageIds,
|
||||
content_id: contentId
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user