diff --git a/backend/igny8_core/ai/ai_core.py b/backend/igny8_core/ai/ai_core.py index 949b7bc2..f0756feb 100644 --- a/backend/igny8_core/ai/ai_core.py +++ b/backend/igny8_core/ai/ai_core.py @@ -485,6 +485,12 @@ class AICore: """Generate image using OpenAI DALL-E""" print(f"[AI][{function_name}] Provider: OpenAI") + # CRITICAL: Truncate prompt to OpenAI's 1000 character limit BEFORE any processing + if len(prompt) > 1000: + print(f"[AI][{function_name}][Warning] Prompt too long ({len(prompt)} chars), truncating to 1000") + prompt = prompt[:997].rsplit(' ', 1)[0] + "..." + print(f"[AI][{function_name}] Truncated prompt length: {len(prompt)}") + api_key = api_key or self._openai_api_key if not api_key: error_msg = 'OpenAI API key not configured' diff --git a/backend/igny8_core/ai/tasks.py b/backend/igny8_core/ai/tasks.py index 3328a2d0..346ef3f0 100644 --- a/backend/igny8_core/ai/tasks.py +++ b/backend/igny8_core/ai/tasks.py @@ -276,16 +276,51 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None # Format template with image prompt from database # Template has placeholders: {image_type}, {post_title}, {image_prompt} + # CRITICAL: OpenAI has strict 1000 character limit for prompts try: + # Get template length to calculate available space + template_placeholder_length = len(image_prompt_template.replace('{image_type}', '').replace('{post_title}', '').replace('{image_prompt}', '')) + + # Truncate post_title aggressively (max 100 chars to leave room) + post_title = content.title or content.meta_title or f"Content #{content.id}" + if len(post_title) > 100: + post_title = post_title[:97] + "..." + + # Calculate max image_prompt length: 1000 - template_text - post_title - safety margin + # Assume template adds ~200 chars, post_title max 100, safety margin 50 = ~650 chars for image_prompt + image_prompt = image.prompt or "" + max_image_prompt_length = 650 + if len(image_prompt) > max_image_prompt_length: + logger.warning(f"Image prompt too long ({len(image_prompt)} chars), truncating to {max_image_prompt_length}") + image_prompt = image_prompt[:max_image_prompt_length].rsplit(' ', 1)[0] + "..." + formatted_prompt = image_prompt_template.format( image_type=image_type, - post_title=content.title or content.meta_title or f"Content #{content.id}", - image_prompt=image.prompt # Read directly from database field + post_title=post_title, + image_prompt=image_prompt ) + + # CRITICAL: Final safety check - ALWAYS truncate to 1000 chars max + if len(formatted_prompt) > 1000: + logger.warning(f"Formatted prompt too long ({len(formatted_prompt)} chars), truncating to 1000") + formatted_prompt = formatted_prompt[:997].rsplit(' ', 1)[0] + "..." + + # Double-check after truncation + if len(formatted_prompt) > 1000: + logger.error(f"Prompt still too long after truncation ({len(formatted_prompt)} chars), forcing hard truncate") + formatted_prompt = formatted_prompt[:1000] + except Exception as e: # Fallback if template formatting fails logger.warning(f"Prompt template formatting failed: {e}, using image prompt directly") - formatted_prompt = image.prompt + formatted_prompt = image.prompt or "" + # CRITICAL: Truncate to 1000 chars even in fallback + if len(formatted_prompt) > 1000: + logger.warning(f"Fallback prompt too long ({len(formatted_prompt)} chars), truncating to 1000") + formatted_prompt = formatted_prompt[:997].rsplit(' ', 1)[0] + "..." + # Final hard truncate if still too long + if len(formatted_prompt) > 1000: + formatted_prompt = formatted_prompt[:1000] # Generate image (using same approach as test image generation) logger.info(f"[process_image_generation_queue] Generating image {index}/{total_images} (ID: {image_id})") @@ -307,31 +342,51 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None # Check for errors if result.get('error'): - logger.error(f"Image generation failed for {image_id}: {result.get('error')}") + error_message = result.get('error', 'Unknown error') + logger.error(f"Image generation failed for {image_id}: {error_message}") + + # Truncate error message to avoid database field length issues + # Some database fields may have 200 char limit, so truncate to 180 to be safe + truncated_error = error_message[:180] if len(error_message) > 180 else error_message + # Update image record: failed - image.status = 'failed' - image.save(update_fields=['status']) + try: + image.status = 'failed' + image.save(update_fields=['status']) + except Exception as save_error: + logger.error(f"Failed to save image status to database: {save_error}", exc_info=True) + # Continue even if save fails results.append({ 'image_id': image_id, 'status': 'failed', - 'error': result.get('error') + 'error': truncated_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 + try: + image.image_url = result.get('url') + image.status = 'generated' + image.save(update_fields=['image_url', 'status']) + except Exception as save_error: + logger.error(f"Failed to save image URL/status to database: {save_error}", exc_info=True) + # Continue even if save fails, but mark as failed in results + results.append({ + 'image_id': image_id, + 'status': 'failed', + 'error': f'Database save error: {str(save_error)[:200]}' + }) + failed += 1 + else: + 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)