diff --git a/backend/igny8_core/ai/ai_core.py b/backend/igny8_core/ai/ai_core.py index f0756feb..41ee7680 100644 --- a/backend/igny8_core/ai/ai_core.py +++ b/backend/igny8_core/ai/ai_core.py @@ -488,8 +488,18 @@ class AICore: # 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] + "..." + # Try word-aware truncation, but fallback to hard truncate if no space found + truncated = prompt[:997] + last_space = truncated.rfind(' ') + if last_space > 900: # Only use word-aware if we have a reasonable space + prompt = truncated[:last_space] + "..." + else: + prompt = prompt[:1000] # Hard truncate if no good space found print(f"[AI][{function_name}] Truncated prompt length: {len(prompt)}") + # Final safety check + if len(prompt) > 1000: + prompt = prompt[:1000] + print(f"[AI][{function_name}][Error] Had to hard truncate to exactly 1000 chars") api_key = api_key or self._openai_api_key if not api_key: diff --git a/backend/igny8_core/ai/tasks.py b/backend/igny8_core/ai/tasks.py index 346ef3f0..f8a6a76d 100644 --- a/backend/igny8_core/ai/tasks.py +++ b/backend/igny8_core/ai/tasks.py @@ -275,57 +275,110 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None continue # Format template with image prompt from database - # Template has placeholders: {image_type}, {post_title}, {image_prompt} + # For DALL-E 2: Use image prompt directly (no template) + # For DALL-E 3 and others: Use template with placeholders # 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}', '')) + image_prompt = image.prompt or "" + + if model == 'dall-e-2': + # DALL-E 2: Use image prompt directly, no template + logger.info(f"[process_image_generation_queue] Using DALL-E 2 - skipping template, using image prompt directly") + formatted_prompt = 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=post_title, - image_prompt=image_prompt - ) - - # CRITICAL: Final safety check - ALWAYS truncate to 1000 chars max + # Truncate to 1000 chars if needed 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] + logger.warning(f"DALL-E 2 prompt too long ({len(formatted_prompt)} chars), truncating to 1000") + truncated = formatted_prompt[:997] + last_space = truncated.rfind(' ') + if last_space > 900: + formatted_prompt = truncated[:last_space] + "..." + else: + formatted_prompt = formatted_prompt[:1000] + else: + # DALL-E 3 and others: Use template + try: + # Truncate post_title aggressively (max 80 chars to leave more room for image_prompt) + post_title = content.title or content.meta_title or f"Content #{content.id}" + if len(post_title) > 80: + post_title = post_title[:77] + "..." - 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 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] + # Calculate actual template length with placeholders filled + # Format template with dummy values to measure actual length + template_with_dummies = image_prompt_template.format( + image_type=image_type, + post_title='X' * len(post_title), # Use same length as actual post_title + image_prompt='' # Empty to measure template overhead + ) + template_overhead = len(template_with_dummies) + + # Calculate max image_prompt length: 1000 - template_overhead - safety margin (20) + max_image_prompt_length = 1000 - template_overhead - 20 + if max_image_prompt_length < 50: + # If template is too long, use minimum 50 chars for image_prompt + max_image_prompt_length = 50 + logger.warning(f"Template is very long ({template_overhead} chars), limiting image_prompt to {max_image_prompt_length}") + + logger.info(f"[process_image_generation_queue] Template overhead: {template_overhead} chars, max image_prompt: {max_image_prompt_length} chars") + + # Truncate image_prompt to calculated max + 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}") + # Word-aware truncation + truncated = image_prompt[:max_image_prompt_length - 3] + last_space = truncated.rfind(' ') + if last_space > max_image_prompt_length * 0.8: # Only if we have a reasonable space + image_prompt = truncated[:last_space] + "..." + else: + image_prompt = image_prompt[:max_image_prompt_length - 3] + "..." + + formatted_prompt = image_prompt_template.format( + image_type=image_type, + 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") + # Try word-aware truncation + truncated = formatted_prompt[:997] + last_space = truncated.rfind(' ') + if last_space > 900: # Only use word-aware if we have a reasonable space + formatted_prompt = truncated[:last_space] + "..." + else: + formatted_prompt = formatted_prompt[:1000] # Hard truncate + + # Double-check after truncation - MUST be <= 1000 + 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 + # 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") + # Try word-aware truncation + truncated = formatted_prompt[:997] + last_space = truncated.rfind(' ') + if last_space > 900: + formatted_prompt = truncated[:last_space] + "..." + else: + formatted_prompt = formatted_prompt[:1000] # Hard truncate + # Final hard truncate if still too long - MUST be <= 1000 + 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})") logger.info(f"[process_image_generation_queue] Provider: {provider}, Model: {model}") - logger.info(f"[process_image_generation_queue] Prompt length: {len(formatted_prompt)}") + logger.info(f"[process_image_generation_queue] Prompt length: {len(formatted_prompt)} (MUST be <= 1000)") + if len(formatted_prompt) > 1000: + logger.error(f"[process_image_generation_queue] ERROR: Prompt is {len(formatted_prompt)} chars, truncating NOW!") + formatted_prompt = formatted_prompt[:1000] + logger.info(f"[process_image_generation_queue] Final prompt length: {len(formatted_prompt)}") logger.info(f"[process_image_generation_queue] Image type: {image_type}") result = ai_core.generate_image( diff --git a/frontend/src/components/common/ImageQueueModal.tsx b/frontend/src/components/common/ImageQueueModal.tsx index 13e245d9..a32ad89e 100644 --- a/frontend/src/components/common/ImageQueueModal.tsx +++ b/frontend/src/components/common/ImageQueueModal.tsx @@ -28,6 +28,8 @@ interface ImageQueueModalProps { queue: ImageQueueItem[]; totalImages: number; taskId?: string | null; + model?: string; + provider?: string; onUpdateQueue?: (queue: ImageQueueItem[]) => void; onLog?: (log: { timestamp: string; @@ -45,6 +47,8 @@ export default function ImageQueueModal({ queue, totalImages, taskId, + model, + provider, onUpdateQueue, onLog, }: ImageQueueModalProps) { @@ -306,6 +310,11 @@ export default function ImageQueueModal({
Total: {totalImages} image{totalImages !== 1 ? 's' : ''} in queue
+ {model && ( ++ Model: {provider === 'openai' ? 'OpenAI' : provider === 'runware' ? 'Runware' : provider || 'Unknown'} {model === 'dall-e-2' ? 'DALL·E 2' : model === 'dall-e-3' ? 'DALL·E 3' : model} +
+ )} diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index edba45bd..78ebe6b1 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -73,6 +73,8 @@ export default function Images() { const [imageQueue, setImageQueue] = useState