diff --git a/EXPECTED_RESPONSE_STRUCTURE.md b/EXPECTED_RESPONSE_STRUCTURE.md deleted file mode 100644 index a64c3966..00000000 --- a/EXPECTED_RESPONSE_STRUCTURE.md +++ /dev/null @@ -1,107 +0,0 @@ -# Expected Response Structure for generate_images_from_prompts - -## Function Flow - -1. **Frontend calls**: `POST /v1/writer/images/generate_images/` with `{ ids: [1, 2, 3] }` -2. **Backend ViewSet** (`ImagesViewSet.generate_images`): - - Calls `run_ai_task` with `function_name='generate_images_from_prompts'` - - Returns response with `queued_prompts` if in TEST MODE - -3. **AIEngine.execute**: - - Validates, prepares, builds prompt (placeholder), parses (placeholder) - - Calls `save_output` which queues prompts (TEST MODE) - -4. **GenerateImagesFromPromptsFunction.save_output**: - - Returns dict with queued prompts - -## Response Structure (TEST MODE) - -```json -{ - "success": true, - "count": 3, - "images_generated": 0, - "images_failed": 0, - "total_images": 3, - "queued_prompts": [ - { - "image_id": 1, - "index": 1, - "image_type": "featured_image", - "content_title": "Your Content Title", - "provider": "openai", - "model": "dall-e-2", - "formatted_prompt": "Create a high-quality realistic image to use as a featured photo for a blog post titled \"Your Content Title\". The image should visually represent the theme, mood, and subject implied by the image prompt: [original prompt]. Focus on a realistic, well-composed scene that naturally communicates the topic without text or logos...", - "negative_prompt": null, - "prompt_length": 250 - }, - { - "image_id": 2, - "index": 2, - "image_type": "in_article_1", - "content_title": "Your Content Title", - "provider": "openai", - "model": "dall-e-2", - "formatted_prompt": "...", - "negative_prompt": null, - "prompt_length": 245 - } - ], - "test_mode": true, - "provider": "openai", - "model": "dall-e-2", - "errors": null -} -``` - -## Response Structure (Production Mode - when AI calls are enabled) - -```json -{ - "success": true, - "count": 3, - "images_generated": 3, - "images_failed": 0, - "total_images": 3, - "errors": null -} -``` - -## Key Points - -1. **TEST MODE** (current): - - `images_generated: 0` (no actual images generated) - - `queued_prompts` array contains all formatted prompts - - `test_mode: true` - - Each prompt includes the complete formatted prompt that would be sent to AI - -2. **Production MODE** (when AI calls are uncommented): - - `images_generated` will be > 0 - - `queued_prompts` will not be in response - - `test_mode: false` or not present - - Images will have `image_url` and `status='generated'` - -## Console Logging - -The function logs to console (if DEBUG_MODE=True): -- `[generate_images_from_prompts] [TEST MODE] Queued prompt X/Y` -- `[generate_images_from_prompts] [TEST MODE] Provider: openai, Model: dall-e-2` -- `[generate_images_from_prompts] [TEST MODE] Prompt length: 250 chars` -- `[generate_images_from_prompts] [TEST MODE] Prompt preview: ...` - -## Frontend Console Output - -When clicking "Generate Images", browser console will show: -``` -[Generate Images] Request: { imageIds: [1, 2, 3], count: 3 } -[Generate Images] Endpoint: /v1/writer/images/generate_images/ -[Generate Images] Full Response: { success: true, queued_prompts: [...], ... } -[Generate Images] Queued Prompts (TEST MODE - NOT sent to AI): [...] -[Generate Images] Provider: openai, Model: dall-e-2 -[Generate Images] Prompt 1/3: - - Image Type: featured_image - - Content: "Your Content Title" - - Prompt Length: 250 chars - - Full Prompt: "Create a high-quality realistic image..." -``` - diff --git a/backend/igny8_core/ai/base.py b/backend/igny8_core/ai/base.py index 0c178c9c..20f79613 100644 --- a/backend/igny8_core/ai/base.py +++ b/backend/igny8_core/ai/base.py @@ -84,8 +84,7 @@ class BaseAIFunction(ABC): original_data: Any, account=None, progress_tracker=None, - step_tracker=None, - console_tracker=None + step_tracker=None ) -> Dict: """ Save parsed results to database. diff --git a/backend/igny8_core/ai/engine.py b/backend/igny8_core/ai/engine.py index 0b62841d..3507f6e1 100644 --- a/backend/igny8_core/ai/engine.py +++ b/backend/igny8_core/ai/engine.py @@ -4,7 +4,7 @@ AI Engine - Central orchestrator for all AI functions import logging from typing import Dict, Any, Optional from igny8_core.ai.base import BaseAIFunction -from igny8_core.ai.tracker import StepTracker, ProgressTracker, CostTracker, ConsoleStepTracker +from igny8_core.ai.tracker import StepTracker, ProgressTracker, CostTracker from igny8_core.ai.ai_core import AICore from igny8_core.ai.settings import get_model_config @@ -22,7 +22,6 @@ class AIEngine: self.account = account self.tracker = ProgressTracker(celery_task) self.step_tracker = StepTracker('ai_engine') # For Celery progress callbacks - self.console_tracker = None # Will be initialized per function self.cost_tracker = CostTracker() def _get_input_description(self, function_name: str, payload: dict, count: int) -> str: @@ -81,12 +80,6 @@ class AIEngine: total_images = 1 + max_images return f"Mapping Content for {total_images} Image Prompts" return f"Mapping Content for Image Prompts" - elif function_name == 'generate_images_from_prompts': - # Extract image count from data - if isinstance(data, dict) and 'images' in data: - total_images = len(data.get('images', [])) - return f"Preparing to generate {total_images} image{'s' if total_images != 1 else ''}" - return f"Preparing image generation queue" return f"Preparing {count} item{'s' if count != 1 else ''}" def _get_ai_call_message(self, function_name: str, count: int) -> str: @@ -99,8 +92,6 @@ class AIEngine: return f"Writing article{'s' if count != 1 else ''} with AI" elif function_name == 'generate_images': return f"Creating image{'s' if count != 1 else ''} with AI" - elif function_name == 'generate_images_from_prompts': - return f"Generating images with AI" return f"Processing with AI" def _get_parse_message(self, function_name: str) -> str: @@ -131,8 +122,6 @@ class AIEngine: if in_article_count > 0: return f"Writing {in_article_count} In‑article Image Prompts" return "Writing In‑article Image Prompts" - elif function_name == 'generate_images_from_prompts': - return f"{count} image{'s' if count != 1 else ''} generated" return f"{count} item{'s' if count != 1 else ''} processed" def _get_save_message(self, function_name: str, count: int) -> str: @@ -148,8 +137,6 @@ class AIEngine: elif function_name == 'generate_image_prompts': # Count is total prompts created return f"Assigning {count} Prompts to Dedicated Slots" - elif function_name == 'generate_images_from_prompts': - return f"Saving {count} image{'s' if count != 1 else ''}" return f"Saving {count} item{'s' if count != 1 else ''}" def execute(self, fn: BaseAIFunction, payload: dict) -> dict: @@ -167,10 +154,6 @@ class AIEngine: function_name = fn.get_name() self.step_tracker.function_name = function_name - # Initialize console tracker for logging (Stage 3 requirement) - self.console_tracker = ConsoleStepTracker(function_name) - self.console_tracker.init(f"Starting {function_name} execution") - try: # Phase 1: INIT - Validation & Setup (0-10%) # Extract input data for user-friendly messages @@ -178,16 +161,12 @@ class AIEngine: input_count = len(ids) if ids else 0 input_description = self._get_input_description(function_name, payload, input_count) - self.console_tracker.prep(f"Validating {input_description}") validated = fn.validate(payload, self.account) if not validated['valid']: - self.console_tracker.error('ValidationError', validated['error']) return self._handle_error(validated['error'], fn) # Build validation message with keyword names for auto_cluster validation_message = self._build_validation_message(function_name, payload, input_count, input_description) - - self.console_tracker.prep("Validation complete") self.step_tracker.add_request_step("INIT", "success", validation_message) self.tracker.update("INIT", 10, validation_message, meta=self.step_tracker.get_meta()) @@ -207,149 +186,127 @@ class AIEngine: data_count = input_count prep_message = self._get_prep_message(function_name, data_count, data) - self.console_tracker.prep(prep_message) - # For image generation, build_prompt returns placeholder - # Actual processing happens in save_output - if function_name == 'generate_images_from_prompts': - prompt = "Image generation queue prepared" - else: - prompt = fn.build_prompt(data, self.account) - self.console_tracker.prep(f"Prompt built: {len(prompt)} characters") + prompt = fn.build_prompt(data, self.account) self.step_tracker.add_request_step("PREP", "success", prep_message) self.tracker.update("PREP", 25, prep_message, meta=self.step_tracker.get_meta()) # Phase 3: AI_CALL - Provider API Call (25-70%) - # For image generation, AI calls happen in save_output, so skip this phase - if function_name == 'generate_images_from_prompts': - # Skip AI_CALL phase - processing happens in save_output - raw_response = {'content': 'Image generation queue ready'} - parsed = {'processed': True} - else: - ai_core = AICore(account=self.account) - function_name = fn.get_name() - - # Generate function_id for tracking (ai-{function_name}-01) - # Normalize underscores to hyphens to match frontend tracking IDs - function_id_base = function_name.replace('_', '-') - function_id = f"ai-{function_id_base}-01-desktop" - - # Get model config from settings (Stage 4 requirement) - # Pass account to read model from IntegrationSettings - model_config = get_model_config(function_name, account=self.account) - model = model_config.get('model') - - # Read model straight from IntegrationSettings for visibility - model_from_integration = None - if self.account: - try: - from igny8_core.modules.system.models import IntegrationSettings - openai_settings = IntegrationSettings.objects.filter( - integration_type='openai', - account=self.account, - is_active=True - ).first() - if openai_settings and openai_settings.config: - model_from_integration = openai_settings.config.get('model') - except Exception as integration_error: - logger.warning( - "[AIEngine] Unable to read model from IntegrationSettings: %s", - integration_error, - exc_info=True, - ) - - # Debug logging: Show model configuration (console only, not in step tracker) - logger.info(f"[AIEngine] Model Configuration for {function_name}:") - logger.info(f" - Model from get_model_config: {model}") - logger.info(f" - Full model_config: {model_config}") - self.console_tracker.ai_call(f"Model from settings: {model_from_integration or 'Not set'}") - self.console_tracker.ai_call(f"Model selected for request: {model or 'default'}") - self.console_tracker.ai_call(f"Calling {model or 'default'} model with {len(prompt)} char prompt") - self.console_tracker.ai_call(f"Function ID: {function_id}") - - # Track AI call start with user-friendly message - ai_call_message = self._get_ai_call_message(function_name, data_count) - self.step_tracker.add_response_step("AI_CALL", "success", ai_call_message) - self.tracker.update("AI_CALL", 50, ai_call_message, meta=self.step_tracker.get_meta()) - + ai_core = AICore(account=self.account) + function_name = fn.get_name() + + # Generate function_id for tracking (ai-{function_name}-01) + # Normalize underscores to hyphens to match frontend tracking IDs + function_id_base = function_name.replace('_', '-') + function_id = f"ai-{function_id_base}-01-desktop" + + # Get model config from settings (Stage 4 requirement) + # Pass account to read model from IntegrationSettings + model_config = get_model_config(function_name, account=self.account) + model = model_config.get('model') + + # Read model straight from IntegrationSettings for visibility + model_from_integration = None + if self.account: try: - # Use centralized run_ai_request() with console logging (Stage 2 & 3 requirement) - # Pass console_tracker for unified logging - raw_response = ai_core.run_ai_request( - prompt=prompt, - model=model, - max_tokens=model_config.get('max_tokens'), - temperature=model_config.get('temperature'), - response_format=model_config.get('response_format'), - function_name=function_name, - function_id=function_id, # Pass function_id for tracking - tracker=self.console_tracker # Pass console tracker for logging + from igny8_core.modules.system.models import IntegrationSettings + openai_settings = IntegrationSettings.objects.filter( + integration_type='openai', + account=self.account, + is_active=True + ).first() + if openai_settings and openai_settings.config: + model_from_integration = openai_settings.config.get('model') + except Exception as integration_error: + logger.warning( + "[AIEngine] Unable to read model from IntegrationSettings: %s", + integration_error, + exc_info=True, ) - except Exception as e: - error_msg = f"AI call failed: {str(e)}" - logger.error(f"Exception during AI call: {error_msg}", exc_info=True) - return self._handle_error(error_msg, fn) - - if raw_response.get('error'): - error_msg = raw_response.get('error', 'Unknown AI error') - logger.error(f"AI call returned error: {error_msg}") - return self._handle_error(error_msg, fn) - - if not raw_response.get('content'): - error_msg = "AI call returned no content" - logger.error(error_msg) - return self._handle_error(error_msg, fn) - - # Track cost - self.cost_tracker.record( + + # Debug logging: Show model configuration (console only, not in step tracker) + logger.info(f"[AIEngine] Model Configuration for {function_name}:") + logger.info(f" - Model from get_model_config: {model}") + logger.info(f" - Full model_config: {model_config}") + + # Track AI call start with user-friendly message + ai_call_message = self._get_ai_call_message(function_name, data_count) + self.step_tracker.add_response_step("AI_CALL", "success", ai_call_message) + self.tracker.update("AI_CALL", 50, ai_call_message, meta=self.step_tracker.get_meta()) + + try: + # Use centralized run_ai_request() + raw_response = ai_core.run_ai_request( + prompt=prompt, + model=model, + max_tokens=model_config.get('max_tokens'), + temperature=model_config.get('temperature'), + response_format=model_config.get('response_format'), function_name=function_name, - cost=raw_response.get('cost', 0), - tokens=raw_response.get('total_tokens', 0), - model=raw_response.get('model') + function_id=function_id # Pass function_id for tracking ) + except Exception as e: + error_msg = f"AI call failed: {str(e)}" + logger.error(f"Exception during AI call: {error_msg}", exc_info=True) + return self._handle_error(error_msg, fn) + + if raw_response.get('error'): + error_msg = raw_response.get('error', 'Unknown AI error') + logger.error(f"AI call returned error: {error_msg}") + return self._handle_error(error_msg, fn) + + if not raw_response.get('content'): + error_msg = "AI call returned no content" + logger.error(error_msg) + return self._handle_error(error_msg, fn) + + # Track cost + self.cost_tracker.record( + function_name=function_name, + cost=raw_response.get('cost', 0), + tokens=raw_response.get('total_tokens', 0), + model=raw_response.get('model') + ) + + # Update AI_CALL step with results + self.step_tracker.response_steps[-1] = { + **self.step_tracker.response_steps[-1], + 'message': f"Received {raw_response.get('total_tokens', 0)} tokens, Cost: ${raw_response.get('cost', 0):.6f}", + 'duration': raw_response.get('duration') + } + self.tracker.update("AI_CALL", 70, f"AI response received ({raw_response.get('total_tokens', 0)} tokens)", meta=self.step_tracker.get_meta()) + + # Phase 4: PARSE - Response Parsing (70-85%) + try: + parse_message = self._get_parse_message(function_name) + response_content = raw_response.get('content', '') + parsed = fn.parse_response(response_content, self.step_tracker) - # Update AI_CALL step with results - self.step_tracker.response_steps[-1] = { - **self.step_tracker.response_steps[-1], - 'message': f"Received {raw_response.get('total_tokens', 0)} tokens, Cost: ${raw_response.get('cost', 0):.6f}", - 'duration': raw_response.get('duration') - } - self.tracker.update("AI_CALL", 70, f"AI response received ({raw_response.get('total_tokens', 0)} tokens)", meta=self.step_tracker.get_meta()) - - # Phase 4: PARSE - Response Parsing (70-85%) - try: - parse_message = self._get_parse_message(function_name) - self.console_tracker.parse(parse_message) - response_content = raw_response.get('content', '') - parsed = fn.parse_response(response_content, self.step_tracker) - - if isinstance(parsed, (list, tuple)): - parsed_count = len(parsed) - elif isinstance(parsed, dict): - # Check if it's a content dict (has 'content' field) or a result dict (has 'count') - if 'content' in parsed: - parsed_count = 1 # Single content item - else: - parsed_count = parsed.get('count', 1) + if isinstance(parsed, (list, tuple)): + parsed_count = len(parsed) + elif isinstance(parsed, dict): + # Check if it's a content dict (has 'content' field) or a result dict (has 'count') + if 'content' in parsed: + parsed_count = 1 # Single content item else: - parsed_count = 1 - - # Update parse message with count for better UX - parse_message = self._get_parse_message_with_count(function_name, parsed_count) - - self.console_tracker.parse(f"Successfully parsed {parsed_count} items from response") - self.step_tracker.add_response_step("PARSE", "success", parse_message) - self.tracker.update("PARSE", 85, parse_message, meta=self.step_tracker.get_meta()) - except Exception as parse_error: - error_msg = f"Failed to parse AI response: {str(parse_error)}" - logger.error(f"AIEngine: {error_msg}", exc_info=True) - logger.error(f"AIEngine: Response content was: {response_content[:500] if response_content else 'None'}...") - return self._handle_error(error_msg, fn) + parsed_count = parsed.get('count', 1) + else: + parsed_count = 1 + + # Update parse message with count for better UX + parse_message = self._get_parse_message_with_count(function_name, parsed_count) + + self.step_tracker.add_response_step("PARSE", "success", parse_message) + self.tracker.update("PARSE", 85, parse_message, meta=self.step_tracker.get_meta()) + except Exception as parse_error: + error_msg = f"Failed to parse AI response: {str(parse_error)}" + logger.error(f"AIEngine: {error_msg}", exc_info=True) + logger.error(f"AIEngine: Response content was: {response_content[:500] if response_content else 'None'}...") + return self._handle_error(error_msg, fn) # Phase 5: SAVE - Database Operations (85-98%) - # Pass step_tracker and console_tracker to save_output so it can add validation steps and log - save_result = fn.save_output(parsed, data, self.account, self.tracker, step_tracker=self.step_tracker, console_tracker=self.console_tracker) + save_result = fn.save_output(parsed, data, self.account, self.tracker, step_tracker=self.step_tracker) clusters_created = save_result.get('clusters_created', 0) keywords_updated = save_result.get('keywords_updated', 0) count = save_result.get('count', 0) @@ -362,7 +319,6 @@ class AIEngine: else: save_msg = self._get_save_message(function_name, data_count) - self.console_tracker.save(save_msg) self.step_tracker.add_request_step("SAVE", "success", save_msg) self.tracker.update("SAVE", 98, save_msg, meta=self.step_tracker.get_meta()) @@ -403,7 +359,6 @@ class AIEngine: # Phase 6: DONE - Finalization (98-100%) success_msg = f"Task completed: {final_save_msg}" if 'final_save_msg' in locals() else "Task completed successfully" - self.console_tracker.done(success_msg) self.step_tracker.add_request_step("DONE", "success", "Task completed successfully") self.tracker.update("DONE", 100, "Task complete!", meta=self.step_tracker.get_meta()) @@ -427,11 +382,6 @@ class AIEngine: """Centralized error handling""" function_name = fn.get_name() if fn else 'unknown' - # Log to console tracker if available (Stage 3 requirement) - if self.console_tracker: - error_type = type(error).__name__ if isinstance(error, Exception) else 'Error' - self.console_tracker.error(error_type, str(error), exception=error if isinstance(error, Exception) else None) - self.step_tracker.add_request_step("Error", "error", error, error=error) error_meta = { diff --git a/backend/igny8_core/ai/functions/__init__.py b/backend/igny8_core/ai/functions/__init__.py index 393a27a4..b308eb38 100644 --- a/backend/igny8_core/ai/functions/__init__.py +++ b/backend/igny8_core/ai/functions/__init__.py @@ -6,7 +6,6 @@ from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction from igny8_core.ai.functions.generate_content import GenerateContentFunction from igny8_core.ai.functions.generate_images import GenerateImagesFunction, generate_images_core from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction -from igny8_core.ai.functions.generate_images_from_prompts import GenerateImagesFromPromptsFunction __all__ = [ 'AutoClusterFunction', @@ -15,5 +14,4 @@ __all__ = [ 'GenerateImagesFunction', 'generate_images_core', 'GenerateImagePromptsFunction', - 'GenerateImagesFromPromptsFunction', ] diff --git a/backend/igny8_core/ai/functions/generate_images_from_prompts.py b/backend/igny8_core/ai/functions/generate_images_from_prompts.py deleted file mode 100644 index 2a54202e..00000000 --- a/backend/igny8_core/ai/functions/generate_images_from_prompts.py +++ /dev/null @@ -1,472 +0,0 @@ -""" -Generate Images from Prompts AI Function -Generates actual images from existing image prompts using AI -""" -import logging -from typing import Dict, List, Any -from django.db import transaction -from igny8_core.ai.base import BaseAIFunction -from igny8_core.modules.writer.models import Images, Content -from igny8_core.ai.ai_core import AICore -from igny8_core.ai.validators import validate_ids -from igny8_core.ai.prompts import PromptRegistry - -logger = logging.getLogger(__name__) - - -class GenerateImagesFromPromptsFunction(BaseAIFunction): - """Generate actual images from image prompts using AI""" - - def get_name(self) -> str: - return 'generate_images_from_prompts' - - def get_metadata(self) -> Dict: - return { - 'display_name': 'Generate Images from Prompts', - 'description': 'Generate actual images from existing image prompts', - 'phases': { - 'INIT': 'Validating image prompts...', - 'PREP': 'Preparing image generation queue...', - 'AI_CALL': 'Generating images with AI...', - 'PARSE': 'Processing image URLs...', - 'SAVE': 'Saving image URLs...', - 'DONE': 'Images generated!' - } - } - - def get_max_items(self) -> int: - return 100 # Max images per batch - - def validate(self, payload: dict, account=None) -> Dict: - """Validate image IDs exist and have prompts""" - result = validate_ids(payload, max_items=self.get_max_items()) - if not result['valid']: - return result - - # Check images exist and have prompts - image_ids = payload.get('ids', []) - if image_ids: - queryset = Images.objects.filter(id__in=image_ids) - if account: - queryset = queryset.filter(account=account) - - images = list(queryset.select_related('content', 'task')) - - if not images: - return { - 'valid': False, - 'error': 'No images found with provided IDs' - } - - # Check all images have prompts - images_without_prompts = [img.id for img in images if not img.prompt or not img.prompt.strip()] - if images_without_prompts: - return { - 'valid': False, - 'error': f'Images {images_without_prompts} do not have prompts' - } - - # Check all images are pending - images_not_pending = [img.id for img in images if img.status != 'pending'] - if images_not_pending: - return { - 'valid': False, - 'error': f'Images {images_not_pending} are not in pending status' - } - - return {'valid': True} - - def prepare(self, payload: dict, account=None) -> Dict: - """Load images and image generation settings""" - image_ids = payload.get('ids', []) - - queryset = Images.objects.filter(id__in=image_ids, status='pending') - if account: - queryset = queryset.filter(account=account) - - images = list(queryset.select_related('content', 'task', 'account', 'site', 'sector')) - - if not images: - raise ValueError("No pending images found with prompts") - - # Get image generation settings - CHECK IF ENABLED - image_settings = {} - image_generation_enabled = False - if account: - try: - from igny8_core.modules.system.models import IntegrationSettings - integration = IntegrationSettings.objects.get( - account=account, - integration_type='image_generation' - ) - image_generation_enabled = integration.is_active - image_settings = integration.config or {} - logger.info(f"[generate_images_from_prompts] Image generation settings: enabled={image_generation_enabled}, config_keys={list(image_settings.keys())}") - except IntegrationSettings.DoesNotExist: - logger.warning(f"[generate_images_from_prompts] Image generation integration not found for account {account.id}") - raise ValueError("Image generation integration not configured") - except Exception as e: - logger.error(f"[generate_images_from_prompts] Failed to load image generation settings: {e}") - raise ValueError(f"Failed to load image generation settings: {str(e)}") - - if not image_generation_enabled: - raise ValueError("Image generation is not enabled in settings") - - # Get provider from image_generation settings - provider = image_settings.get('provider') or image_settings.get('service', 'openai') - logger.info(f"[generate_images_from_prompts] Provider from settings: {provider}") - - # Get provider-specific settings (OpenAI or Runware) - CHECK IF ENABLED - provider_api_key = None - provider_enabled = False - provider_model = None - - if provider == 'openai': - try: - openai_settings = IntegrationSettings.objects.get( - account=account, - integration_type='openai' - ) - provider_enabled = openai_settings.is_active - provider_api_key = openai_settings.config.get('apiKey') if openai_settings.config else None - provider_model = openai_settings.config.get('model') if openai_settings.config else None - logger.info(f"[generate_images_from_prompts] OpenAI settings: enabled={provider_enabled}, has_key={bool(provider_api_key)}, model={provider_model}") - except IntegrationSettings.DoesNotExist: - logger.error(f"[generate_images_from_prompts] OpenAI integration not found") - raise ValueError("OpenAI integration not configured") - except Exception as e: - logger.error(f"[generate_images_from_prompts] Error getting OpenAI settings: {e}") - raise ValueError(f"Failed to load OpenAI settings: {str(e)}") - elif provider == 'runware': - try: - runware_settings = IntegrationSettings.objects.get( - account=account, - integration_type='runware' - ) - provider_enabled = runware_settings.is_active - provider_api_key = runware_settings.config.get('apiKey') if runware_settings.config else None - provider_model = runware_settings.config.get('model') if runware_settings.config else None - logger.info(f"[generate_images_from_prompts] Runware settings: enabled={provider_enabled}, has_key={bool(provider_api_key)}, model={provider_model}") - except IntegrationSettings.DoesNotExist: - logger.error(f"[generate_images_from_prompts] Runware integration not found") - raise ValueError("Runware integration not configured") - except Exception as e: - logger.error(f"[generate_images_from_prompts] Error getting Runware settings: {e}") - raise ValueError(f"Failed to load Runware settings: {str(e)}") - else: - raise ValueError(f"Invalid provider: {provider}") - - # Validate provider is enabled and has API key - if not provider_enabled: - raise ValueError(f"{provider.capitalize()} integration is not enabled") - - if not provider_api_key: - raise ValueError(f"{provider.capitalize()} API key not configured") - - # Determine model: from provider settings, or image_generation settings, or default - if provider_model: - model = provider_model - elif provider == 'runware': - model = image_settings.get('model') or image_settings.get('runwareModel', 'runware:97@1') - else: - model = image_settings.get('model') or image_settings.get('imageModel', 'dall-e-3') - - logger.info(f"[generate_images_from_prompts] Final settings: provider={provider}, model={model}, enabled={provider_enabled}, has_api_key={bool(provider_api_key)}") - - # Get prompt templates - image_prompt_template = PromptRegistry.get_image_prompt_template(account) - negative_prompt = PromptRegistry.get_negative_prompt(account) - - return { - 'images': images, - 'account': account, - 'provider': provider, - 'model': model, - 'api_key': provider_api_key, # Include API key - 'image_type': image_settings.get('image_type', 'realistic'), - 'image_format': image_settings.get('image_format', 'webp'), - 'image_prompt_template': image_prompt_template, - 'negative_prompt': negative_prompt, - } - - def build_prompt(self, data: Dict, account=None) -> str: - """ - Build prompt for AI_CALL phase. - For image generation, we return a placeholder since we process images in save_output. - """ - # Return placeholder - actual processing happens in save_output - return "Image generation queue prepared" - - def parse_response(self, response: str, step_tracker=None) -> Dict: - """ - Parse response from AI_CALL. - For image generation, we process images directly in save_output, so this is a placeholder. - """ - return {'processed': True} - - def save_output( - self, - parsed: Dict, - original_data: Dict, - account=None, - progress_tracker=None, - step_tracker=None, - console_tracker=None - ) -> Dict: - """ - Process all images sequentially and generate them. - This method handles the loop and makes AI calls directly. - """ - function_name = self.get_name() - - if console_tracker: - console_tracker.save(f"[{function_name}] Starting image generation queue") - - images = original_data.get('images', []) - if not images: - error_msg = "[{function_name}] No images to process" - if console_tracker: - console_tracker.error('ValidationError', error_msg) - raise ValueError(error_msg) - - provider = original_data.get('provider', 'openai') - model = original_data.get('model', 'dall-e-3') - api_key = original_data.get('api_key') # Get API key from prepare - image_type = original_data.get('image_type', 'realistic') - image_prompt_template = original_data.get('image_prompt_template', '') - negative_prompt = original_data.get('negative_prompt', '') - - # Validate API key is present - if not api_key: - error_msg = f"[{function_name}] API key not found for provider {provider}" - if console_tracker: - console_tracker.error('ConfigurationError', error_msg) - raise ValueError(error_msg) - - ai_core = AICore(account=account or original_data.get('account')) - - total_images = len(images) - images_generated = 0 - images_failed = 0 - errors = [] - - if console_tracker: - console_tracker.prep(f"[{function_name}] Preparing {total_images} image{'s' if total_images != 1 else ''} for generation") - - # Initialize image queue in meta for frontend - image_queue = [] - for idx, img in enumerate(images, 1): - content_obj = img.content - if not content_obj: - if img.task: - content_title = img.task.title - else: - content_title = "Content" - else: - content_title = content_obj.title or content_obj.meta_title or "Content" - - image_queue.append({ - 'image_id': img.id, - 'index': idx, - 'label': f"{img.image_type.replace('_', ' ').title()} Image", - 'content_title': content_title, - 'status': 'pending', - 'progress': 0, - 'image_url': None, - 'error': None - }) - - # Send initial queue to frontend - if progress_tracker: - initial_meta = step_tracker.get_meta() if step_tracker else {} - initial_meta['image_queue'] = image_queue - progress_tracker.update("PREP", 10, f"Preparing to generate {total_images} image{'s' if total_images != 1 else ''}", meta=initial_meta) - - if console_tracker: - console_tracker.prep(f"[{function_name}] Image queue initialized with {total_images} image{'s' if total_images != 1 else ''}") - console_tracker.prep(f"[{function_name}] Provider: {provider}, Model: {model}, API Key: {'***' + api_key[-4:] if api_key and len(api_key) > 4 else 'NOT SET'}") - - # Queue all prompts first (TEST MODE - don't send to AI) - queued_prompts = [] - - # Process each image sequentially - for index, image in enumerate(images, 1): - queue_item = image_queue[index - 1] - queue_item['status'] = 'processing' - queue_item['progress'] = 0 - - try: - # Get content title - content = image.content - if not content: - # Fallback to task if no content - if image.task: - content_title = image.task.title - else: - content_title = "Content" - else: - content_title = content.title or content.meta_title or "Content" - - if console_tracker: - console_tracker.prep(f"[{function_name}] Processing image {index}/{total_images}: {image.image_type} for '{content_title}'") - - # Format prompt using template - if image_prompt_template: - try: - formatted_prompt = image_prompt_template.format( - post_title=content_title, - image_prompt=image.prompt, - image_type=image_type - ) - if console_tracker: - console_tracker.prep(f"[{function_name}] Formatted prompt using template (length: {len(formatted_prompt)})") - except KeyError as e: - logger.warning(f"Template formatting error: {e}, using simple format") - formatted_prompt = f"Create a high-quality {image_type} image: {image.prompt}" - if console_tracker: - console_tracker.prep(f"[{function_name}] Template formatting error, using fallback prompt") - else: - # Fallback template - formatted_prompt = f"Create a high-quality {image_type} image: {image.prompt}" - if console_tracker: - console_tracker.prep(f"[{function_name}] Using fallback prompt template") - - # Update progress: PREP phase for this image - if progress_tracker and step_tracker: - prep_msg = f"Generating image {index} of {total_images}: {image.image_type}" - step_tracker.add_request_step("PREP", "success", prep_msg) - queue_item['progress'] = 10 - # Update queue in meta - meta = step_tracker.get_meta() - meta['image_queue'] = image_queue - progress_pct = 10 + int((index - 1) / total_images * 15) # 10-25% for PREP - progress_tracker.update("PREP", progress_pct, prep_msg, meta=meta) - - # Generate image - update progress incrementally - if progress_tracker and step_tracker: - ai_msg = f"Generating {image.image_type} image {index} of {total_images} with AI" - step_tracker.add_response_step("AI_CALL", "success", ai_msg) - queue_item['progress'] = 25 - meta = step_tracker.get_meta() - meta['image_queue'] = image_queue - progress_pct = 25 + int((index - 1) / total_images * 45) # 25-70% for AI_CALL - progress_tracker.update("AI_CALL", progress_pct, ai_msg, meta=meta) - - # Update progress to 50% (simulating API call start) - queue_item['progress'] = 50 - if progress_tracker and step_tracker: - meta = step_tracker.get_meta() - meta['image_queue'] = image_queue - progress_tracker.update("AI_CALL", progress_pct, ai_msg, meta=meta) - - # Queue the complete prompt (TEST MODE - don't send to AI yet) - queued_prompts.append({ - 'image_id': image.id, - 'index': index, - 'image_type': image.image_type, - 'content_title': content_title, - 'provider': provider, - 'model': model, - 'formatted_prompt': formatted_prompt, - 'negative_prompt': negative_prompt if provider == 'runware' else None, - 'prompt_length': len(formatted_prompt) - }) - - if console_tracker: - console_tracker.ai_call(f"[{function_name}] [TEST MODE] Queued prompt {index}/{total_images}: {image.image_type} for '{content_title}'") - console_tracker.ai_call(f"[{function_name}] [TEST MODE] Provider: {provider}, Model: {model}") - console_tracker.ai_call(f"[{function_name}] [TEST MODE] Prompt length: {len(formatted_prompt)} chars") - console_tracker.ai_call(f"[{function_name}] [TEST MODE] Prompt preview: {formatted_prompt[:150]}...") - - # TEMPORARY: Simulate result for testing (don't actually call AI) - result = { - 'url': None, - 'error': None, - 'test_mode': True, - 'queued': True - } - - # ACTUAL AI CALL (COMMENTED OUT FOR TESTING) - # if console_tracker: - # console_tracker.ai_call(f"[{function_name}] Calling {provider}/{model} API for image {index}/{total_images}") - # - # result = ai_core.generate_image( - # prompt=formatted_prompt, - # provider=provider, - # model=model, - # size='1024x1024', - # n=1, - # api_key=api_key, # Pass API key explicitly - # negative_prompt=negative_prompt if provider == 'runware' else None, - # function_name='generate_images_from_prompts' - # ) - - # TEST MODE: Mark as queued (not actually generated) - queue_item['status'] = 'completed' - queue_item['progress'] = 100 - queue_item['image_url'] = None # No URL in test mode - - if console_tracker: - console_tracker.parse(f"[{function_name}] [TEST MODE] Prompt queued for image {index}/{total_images}") - console_tracker.save(f"[{function_name}] [TEST MODE] Queued image {index}/{total_images} (ID: {image.id})") - - # Update progress: SAVE phase - if progress_tracker and step_tracker: - save_msg = f"Queued prompt {index} of {total_images} (TEST MODE)" - step_tracker.add_request_step("SAVE", "success", save_msg) - meta = step_tracker.get_meta() - meta['image_queue'] = image_queue - progress_pct = 85 + int((index - 1) / total_images * 13) # 85-98% for SAVE - progress_tracker.update("SAVE", progress_pct, save_msg, meta=meta) - - except Exception as e: - # Mark as failed - queue_item['status'] = 'failed' - queue_item['progress'] = 100 - queue_item['error'] = str(e) - with transaction.atomic(): - image.status = 'failed' - image.save(update_fields=['status', 'updated_at']) - - error_msg = f"[{function_name}] Image {index}/{total_images} exception: {str(e)}" - errors.append(error_msg) - images_failed += 1 - logger.error(f"Exception generating image {image.id}: {str(e)}", exc_info=True) - - if console_tracker: - console_tracker.error('Exception', error_msg) - - continue - - # Log all queued prompts (TEST MODE) - if console_tracker: - console_tracker.save(f"[{function_name}] [TEST MODE] All prompts queued. Total: {len(queued_prompts)} prompts") - console_tracker.save(f"[{function_name}] [TEST MODE] Provider: {provider}, Model: {model}") - for qp in queued_prompts: - console_tracker.save(f"[{function_name}] [TEST MODE] Image {qp['index']}: {qp['image_type']} - '{qp['content_title']}'") - console_tracker.save(f"[{function_name}] [TEST MODE] Prompt ({qp['prompt_length']} chars): {qp['formatted_prompt'][:100]}...") - - # Final progress update - if progress_tracker and step_tracker: - final_msg = f"Queued {len(queued_prompts)} prompts (TEST MODE - not sent to AI)" - step_tracker.add_request_step("SAVE", "success", final_msg) - meta = step_tracker.get_meta() - meta['image_queue'] = image_queue - meta['queued_prompts'] = queued_prompts # Include queued prompts in meta - progress_tracker.update("SAVE", 98, final_msg, meta=meta) - - if console_tracker: - console_tracker.done(f"[{function_name}] [TEST MODE] Queued {len(queued_prompts)}/{total_images} prompts successfully (NOT sent to AI)") - - return { - 'count': len(queued_prompts), - 'images_generated': 0, # 0 because we're in test mode - 'images_failed': 0, - 'total_images': total_images, - 'queued_prompts': queued_prompts, # Return queued prompts - 'test_mode': True, - 'provider': provider, - 'model': model, - 'errors': errors if errors else None - } - diff --git a/backend/igny8_core/ai/registry.py b/backend/igny8_core/ai/registry.py index ec4e0195..fd4da7c2 100644 --- a/backend/igny8_core/ai/registry.py +++ b/backend/igny8_core/ai/registry.py @@ -94,15 +94,9 @@ def _load_generate_image_prompts(): from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction return GenerateImagePromptsFunction -def _load_generate_images_from_prompts(): - """Lazy loader for generate_images_from_prompts function""" - from igny8_core.ai.functions.generate_images_from_prompts import GenerateImagesFromPromptsFunction - return GenerateImagesFromPromptsFunction - register_lazy_function('auto_cluster', _load_auto_cluster) register_lazy_function('generate_ideas', _load_generate_ideas) register_lazy_function('generate_content', _load_generate_content) register_lazy_function('generate_images', _load_generate_images) register_lazy_function('generate_image_prompts', _load_generate_image_prompts) -register_lazy_function('generate_images_from_prompts', _load_generate_images_from_prompts) diff --git a/backend/igny8_core/ai/settings.py b/backend/igny8_core/ai/settings.py index 708d8fbc..5bb73134 100644 --- a/backend/igny8_core/ai/settings.py +++ b/backend/igny8_core/ai/settings.py @@ -40,12 +40,6 @@ MODEL_CONFIG = { "temperature": 0.7, "response_format": {"type": "json_object"}, }, - "generate_images_from_prompts": { - "model": "dall-e-3", # Default, overridden by IntegrationSettings - "max_tokens": None, # Not used for images - "temperature": None, # Not used for images - "response_format": None, # Not used for images - }, } # Function name aliases (for backward compatibility) diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index 12fdc9d4..31583b7b 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -514,73 +514,6 @@ 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 for image records""" - from igny8_core.ai.tasks import run_ai_task - - account = getattr(request, 'account', None) - ids = request.data.get('ids', []) - - if not ids: - return Response({ - 'error': 'No IDs provided', - 'type': 'ValidationError' - }, status=status.HTTP_400_BAD_REQUEST) - - account_id = account.id if account else None - - # Queue Celery task - try: - if hasattr(run_ai_task, 'delay'): - task = run_ai_task.delay( - function_name='generate_images_from_prompts', - payload={'ids': ids}, - account_id=account_id - ) - return Response({ - 'success': True, - 'task_id': str(task.id), - 'message': 'Image generation started' - }, status=status.HTTP_200_OK) - else: - # Fallback to synchronous execution - result = run_ai_task( - function_name='generate_images_from_prompts', - payload={'ids': ids}, - account_id=account_id - ) - if result.get('success'): - # Include queued prompts in response for TEST MODE - response_data = { - 'success': True, - 'images_generated': result.get('images_generated', 0), - 'images_failed': result.get('images_failed', 0), - 'count': result.get('count', 0), - 'total_images': result.get('total_images', 0), - 'message': 'Images generated successfully' - } - # Add test mode data if available - if result.get('queued_prompts'): - response_data['queued_prompts'] = result.get('queued_prompts') - response_data['test_mode'] = result.get('test_mode', False) - response_data['provider'] = result.get('provider') - response_data['model'] = result.get('model') - - logger.info(f"[generate_images] Response: {response_data}") - return Response(response_data, status=status.HTTP_200_OK) - else: - return Response({ - 'error': result.get('error', 'Image generation failed'), - 'type': 'TaskExecutionError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - except Exception as e: - return Response({ - 'error': str(e), - 'type': 'ExecutionError' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - class ContentViewSet(SiteSectorModelViewSet): """ ViewSet for managing task content diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 311bf0e0..9dc0916a 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -221,6 +221,7 @@ export default function Images() { return; } +<<<<<<< HEAD // STAGE 1: Open modal immediately with all progress bars setImageQueue(queue); setCurrentContentId(contentId); @@ -241,6 +242,26 @@ export default function Images() { } catch (error: any) { console.error('[Generate Images] Exception:', error); toast.error(`Failed to initialize image generation: ${error.message}`); +======= + const result = await generateImages(imageIds); + if (result.success) { + // Show toast message (no progress modal) + const generated = result.images_generated || 0; + const failed = result.images_failed || 0; + if (generated > 0) { + toast.success(`Images generated: ${generated} image${generated !== 1 ? 's' : ''} created${failed > 0 ? `, ${failed} failed` : ''}`); + } else if (failed > 0) { + toast.error(`Image generation failed: ${failed} image${failed !== 1 ? 's' : ''} failed`); + } else { + toast.success('Image generation completed'); + } + loadImages(); // Reload to show new images + } else { + toast.error(result.error || 'Failed to generate images'); + } + } catch (error: any) { + toast.error(`Failed to generate images: ${error.message}`); +>>>>>>> parent of e89eaab0 (some changes) } }, [toast, images, buildImageQueue]); diff --git a/test_image_generation.py b/test_image_generation.py deleted file mode 100644 index e2277c38..00000000 --- a/test_image_generation.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script for generate_images_from_prompts function -Run this to test the function and see the response -""" -import os -import sys -import django - -# Setup Django -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend')) -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') -django.setup() - -from igny8_core.ai.functions.generate_images_from_prompts import GenerateImagesFromPromptsFunction -from igny8_core.ai.engine import AIEngine -from igny8_core.modules.writer.models import Images -from igny8_core.auth.models import Account - -def test_generate_images_from_prompts(): - """Test the generate_images_from_prompts function""" - print("=" * 80) - print("Testing generate_images_from_prompts function") - print("=" * 80) - - # Get first account - try: - account = Account.objects.first() - if not account: - print("ERROR: No accounts found in database") - return - print(f"Using account: {account.name} (ID: {account.id})") - except Exception as e: - print(f"ERROR: Failed to get account: {e}") - return - - # Get pending images with prompts - try: - images = Images.objects.filter( - account=account, - status='pending' - ).exclude(prompt__isnull=True).exclude(prompt='')[:3] # Get first 3 - - if not images.exists(): - print("ERROR: No pending images with prompts found") - print("Please create some images with prompts first") - return - - image_ids = list(images.values_list('id', flat=True)) - print(f"\nFound {len(image_ids)} pending images: {image_ids}") - - # Show image details - for img in images: - content_title = "Unknown" - if img.content: - content_title = img.content.title or img.content.meta_title or "No title" - elif img.task: - content_title = img.task.title or "No title" - print(f" - Image ID {img.id}: {img.image_type} for '{content_title}'") - print(f" Prompt: {img.prompt[:100] if img.prompt else 'None'}...") - - except Exception as e: - print(f"ERROR: Failed to get images: {e}") - import traceback - traceback.print_exc() - return - - # Create function instance - try: - fn = GenerateImagesFromPromptsFunction() - print(f"\nFunction created: {fn.get_name()}") - except Exception as e: - print(f"ERROR: Failed to create function: {e}") - import traceback - traceback.print_exc() - return - - # Validate - try: - print("\n" + "=" * 80) - print("Step 1: VALIDATE") - print("=" * 80) - validation = fn.validate({'ids': image_ids}, account=account) - print(f"Validation result: {validation}") - if not validation.get('valid'): - print(f"ERROR: Validation failed: {validation.get('error')}") - return - except Exception as e: - print(f"ERROR: Validation failed: {e}") - import traceback - traceback.print_exc() - return - - # Prepare - try: - print("\n" + "=" * 80) - print("Step 2: PREPARE") - print("=" * 80) - prepared = fn.prepare({'ids': image_ids}, account=account) - print(f"Prepared data keys: {list(prepared.keys())}") - print(f"Provider: {prepared.get('provider')}") - print(f"Model: {prepared.get('model')}") - print(f"API Key: {'***' + prepared.get('api_key', '')[-4:] if prepared.get('api_key') and len(prepared.get('api_key', '')) > 4 else 'NOT SET'}") - print(f"Images count: {len(prepared.get('images', []))}") - print(f"Has prompt template: {bool(prepared.get('image_prompt_template'))}") - print(f"Has negative prompt: {bool(prepared.get('negative_prompt'))}") - except Exception as e: - print(f"ERROR: Prepare failed: {e}") - import traceback - traceback.print_exc() - return - - # Build prompt (placeholder for this function) - try: - print("\n" + "=" * 80) - print("Step 3: BUILD PROMPT") - print("=" * 80) - prompt = fn.build_prompt(prepared, account=account) - print(f"Prompt (placeholder): {prompt}") - except Exception as e: - print(f"ERROR: Build prompt failed: {e}") - import traceback - traceback.print_exc() - return - - # Execute via AIEngine (simulated, no actual Celery) - try: - print("\n" + "=" * 80) - print("Step 4: EXECUTE (via AIEngine - TEST MODE)") - print("=" * 80) - print("Note: This will queue prompts but NOT send to AI (TEST MODE)") - - engine = AIEngine(account=account) - result = engine.execute(fn, {'ids': image_ids}) - - print("\n" + "=" * 80) - print("EXECUTION RESULT") - print("=" * 80) - print(f"Success: {result.get('success')}") - print(f"Keys in result: {list(result.keys())}") - - if result.get('success'): - print(f"\nCount: {result.get('count', 0)}") - print(f"Images generated: {result.get('images_generated', 0)}") - print(f"Images failed: {result.get('images_failed', 0)}") - print(f"Total images: {result.get('total_images', 0)}") - print(f"Test mode: {result.get('test_mode', False)}") - print(f"Provider: {result.get('provider')}") - print(f"Model: {result.get('model')}") - - queued_prompts = result.get('queued_prompts', []) - if queued_prompts: - print(f"\nQueued Prompts ({len(queued_prompts)}):") - for idx, qp in enumerate(queued_prompts, 1): - print(f"\n Prompt {idx}/{len(queued_prompts)}:") - print(f" Image ID: {qp.get('image_id')}") - print(f" Image Type: {qp.get('image_type')}") - print(f" Content Title: {qp.get('content_title')}") - print(f" Provider: {qp.get('provider')}") - print(f" Model: {qp.get('model')}") - print(f" Prompt Length: {qp.get('prompt_length')} chars") - print(f" Full Prompt:") - print(f" {qp.get('formatted_prompt', '')[:200]}...") - if qp.get('negative_prompt'): - print(f" Negative Prompt: {qp.get('negative_prompt')}") - else: - print(f"\nError: {result.get('error')}") - print(f"Error type: {result.get('error_type')}") - - print("\n" + "=" * 80) - print("FULL RESULT JSON (for frontend)") - print("=" * 80) - import json - print(json.dumps(result, indent=2, default=str)) - - except Exception as e: - print(f"ERROR: Execution failed: {e}") - import traceback - traceback.print_exc() - return - -if __name__ == '__main__': - test_generate_images_from_prompts() -