diff --git a/requirements.txt b/requirements.txt index 4798c60..f9e8416 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ beautifulsoup4==4.14.2 # AI/ML openai==2.5.0 fal-client==0.9.1 +Pillow>=10.0.0 # Testing pytest==8.4.2 pytest-asyncio==0.21.1 diff --git a/scripts/test_image_generation.py b/scripts/test_image_generation.py index d427524..933adf5 100644 --- a/scripts/test_image_generation.py +++ b/scripts/test_image_generation.py @@ -120,9 +120,8 @@ def test_image_generation(project_id: int): try: # Show the prompt that will be used theme = image_generator.get_theme_prompt(project_id) - title_short = truncate_title(article.title, 4) - hero_prompt = f"{theme} Text: '{title_short}' in clean simple uppercase letters, positioned in middle of image." - click.echo(f" Prompt: {hero_prompt}") + click.echo(f" Prompt: {theme}") + click.echo(f" Title (will be overlaid): {article.title}") hero_image = image_generator.generate_hero_image( project_id=project_id, @@ -234,9 +233,8 @@ def test_image_generation(project_id: int): try: # Show the prompt that will be used theme = image_generator.get_theme_prompt(project_id) - title_short = truncate_title(article.title, 4) - hero_prompt = f"{theme} Text: '{title_short}' in clean simple uppercase letters, positioned in middle of image." - click.echo(f" Prompt: {hero_prompt}") + click.echo(f" Prompt: {theme}") + click.echo(f" Title (will be overlaid): {article.title}") hero_image = image_generator.generate_hero_image( project_id=project_id, diff --git a/src/generation/batch_processor.py b/src/generation/batch_processor.py index 52bf97e..2c72878 100644 --- a/src/generation/batch_processor.py +++ b/src/generation/batch_processor.py @@ -482,7 +482,7 @@ class BatchProcessor: img_url = upload_image_to_storage(storage_client, site, content_image, file_path) if img_url: content_image_urls.append(img_url) - click.echo(f"{prefix} Content image {i+1}/{num_images} uploaded") + click.echo(f"{prefix} Content image {i+1}/{num_images} uploaded: {img_url}") except Exception as e: click.echo(f"{prefix} Content image {i+1} generation failed: {e}") except Exception as e: diff --git a/src/generation/image_generator.py b/src/generation/image_generator.py index 6fb913f..507372f 100644 --- a/src/generation/image_generator.py +++ b/src/generation/image_generator.py @@ -6,9 +6,11 @@ import os import re import random import logging +import io import requests from typing import Optional, Tuple from concurrent.futures import ThreadPoolExecutor, as_completed +from PIL import Image, ImageDraw, ImageFont import fal_client from src.generation.ai_client import AIClient, PromptManager from src.database.repositories import ProjectRepository @@ -75,7 +77,7 @@ class ImageGenerator: theme_prompt, _ = self.ai_client.generate_completion( prompt=user_prompt, system_message=system_msg, - max_tokens=200, + max_tokens=300, temperature=0.7 ) @@ -86,6 +88,94 @@ class ImageGenerator: logger.info(f"Generated theme prompt for project {project_id}") return project.image_theme_prompt + def _overlay_text_on_image( + self, + image_bytes: bytes, + text: str, + width: int, + height: int + ) -> bytes: + """Overlay text on image using PIL""" + img = Image.open(io.BytesIO(image_bytes)) + if img.mode != 'RGBA': + img = img.convert('RGBA') + + draw = ImageDraw.Draw(img) + + try: + font_size = width // 15 + font = ImageFont.truetype("arial.ttf", font_size) + except: + font = ImageFont.load_default() + + # Wrap text to fit within 80% of image width + max_width = int(width * 0.8) + words = text.split() + lines = [] + current_line = [] + + for word in words: + test_line = " ".join(current_line + [word]) + bbox = draw.textbbox((0, 0), test_line, font=font) + if bbox[2] - bbox[0] <= max_width: + current_line.append(word) + else: + if current_line: + lines.append(" ".join(current_line)) + current_line = [word] + if current_line: + lines.append(" ".join(current_line)) + + # Calculate text position (centered) + line_height = draw.textbbox((0, 0), "A", font=font)[3] - draw.textbbox((0, 0), "A", font=font)[1] + line_spacing = int(line_height * 1.3) # Add 30% spacing between lines + total_height = len(lines) * line_spacing + y_start = (height - total_height) // 2 + + # Calculate bounding box for all text + max_line_width = 0 + for line in lines: + bbox = draw.textbbox((0, 0), line, font=font) + line_width = bbox[2] - bbox[0] + if line_width > max_line_width: + max_line_width = line_width + + # Draw semi-transparent dark background box + padding = 20 + box_x1 = (width - max_line_width) // 2 - padding + box_y1 = y_start - padding + box_x2 = (width + max_line_width) // 2 + padding + box_y2 = y_start + total_height + padding + + overlay = Image.new('RGBA', img.size, (0, 0, 0, 0)) + overlay_draw = ImageDraw.Draw(overlay) + overlay_draw.rectangle([box_x1, box_y1, box_x2, box_y2], fill=(0, 0, 0, 180)) + img = Image.alpha_composite(img, overlay) + draw = ImageDraw.Draw(img) + + # Draw each line + y = y_start + for line in lines: + bbox = draw.textbbox((0, 0), line, font=font) + text_width = bbox[2] - bbox[0] + x = (width - text_width) // 2 + + # Draw text with black outline for contrast + for adj in range(-2, 3): + for adj2 in range(-2, 3): + draw.text((x + adj, y + adj2), line, font=font, fill="black") + draw.text((x, y), line, font=font, fill="white") + y += line_spacing + + # Convert back to RGB for JPEG + if img.mode == 'RGBA': + img = img.convert('RGB') + + # Save to bytes + output = io.BytesIO() + img.save(output, format='JPEG', quality=95) + return output.getvalue() + def generate_hero_image( self, project_id: int, @@ -100,8 +190,7 @@ class ImageGenerator: try: theme = self.get_theme_prompt(project_id) - title_short = truncate_title(title, 4) - prompt = f"{theme} Text: '{title_short}' in clean simple uppercase letters, positioned in middle of image." + prompt = theme logger.info(f"Generating hero image with prompt: {prompt}") @@ -141,7 +230,11 @@ class ImageGenerator: logger.info(f"Downloading hero image from URL: {image_url}") response = requests.get(image_url, timeout=30) response.raise_for_status() - return response.content + image_bytes = response.content + + # Overlay text on image + image_bytes = self._overlay_text_on_image(image_bytes, title, width, height) + return image_bytes logger.error(f"No image returned from fal.ai. Response: {result}") return None diff --git a/src/generation/prompts/h2-prompts b/src/generation/prompts/h2-prompts deleted file mode 100644 index 356628a..0000000 --- a/src/generation/prompts/h2-prompts +++ /dev/null @@ -1,9 +0,0 @@ -h2_prompts = { - "general": "Clean, professional illustration related to {h2_topic}, modern business style, simple geometric shapes, corporate color palette, minimalist design, high-quality vector art style", - - "technical": "Technical diagram or infographic about {h2_topic}, clean lines, professional schematic style, industrial design, blue and gray tones, modern technical illustration", - - "process": "Step-by-step process visualization for {h2_topic}, clean flowchart style, professional arrows and connections, corporate color scheme, modern infographic design", - - "benefits": "Professional icon-based illustration showing {h2_topic}, clean symbol design, business-friendly colors, modern flat design style, organized layout" -} \ No newline at end of file