Implement PIL text overlay for hero images

- Add Pillow dependency for image processing
- Remove text instructions from fal.ai prompts (generate clean images)
- Add semi-transparent dark background box behind text for readability
- Overlay full title text with white text and black outline
- Add proper line spacing between text lines
- Fix FAL_KEY environment variable setup
- Add image URL logging to console output during batch processing
- Remove unused h2-prompts file
main
PeninsulaInd 2025-11-19 17:28:26 -06:00
parent 8379313f51
commit be03594fc7
5 changed files with 103 additions and 20 deletions

View File

@ -33,6 +33,7 @@ beautifulsoup4==4.14.2
# AI/ML # AI/ML
openai==2.5.0 openai==2.5.0
fal-client==0.9.1 fal-client==0.9.1
Pillow>=10.0.0
# Testing # Testing
pytest==8.4.2 pytest==8.4.2
pytest-asyncio==0.21.1 pytest-asyncio==0.21.1

View File

@ -120,9 +120,8 @@ def test_image_generation(project_id: int):
try: try:
# Show the prompt that will be used # Show the prompt that will be used
theme = image_generator.get_theme_prompt(project_id) theme = image_generator.get_theme_prompt(project_id)
title_short = truncate_title(article.title, 4) click.echo(f" Prompt: {theme}")
hero_prompt = f"{theme} Text: '{title_short}' in clean simple uppercase letters, positioned in middle of image." click.echo(f" Title (will be overlaid): {article.title}")
click.echo(f" Prompt: {hero_prompt}")
hero_image = image_generator.generate_hero_image( hero_image = image_generator.generate_hero_image(
project_id=project_id, project_id=project_id,
@ -234,9 +233,8 @@ def test_image_generation(project_id: int):
try: try:
# Show the prompt that will be used # Show the prompt that will be used
theme = image_generator.get_theme_prompt(project_id) theme = image_generator.get_theme_prompt(project_id)
title_short = truncate_title(article.title, 4) click.echo(f" Prompt: {theme}")
hero_prompt = f"{theme} Text: '{title_short}' in clean simple uppercase letters, positioned in middle of image." click.echo(f" Title (will be overlaid): {article.title}")
click.echo(f" Prompt: {hero_prompt}")
hero_image = image_generator.generate_hero_image( hero_image = image_generator.generate_hero_image(
project_id=project_id, project_id=project_id,

View File

@ -482,7 +482,7 @@ class BatchProcessor:
img_url = upload_image_to_storage(storage_client, site, content_image, file_path) img_url = upload_image_to_storage(storage_client, site, content_image, file_path)
if img_url: if img_url:
content_image_urls.append(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: except Exception as e:
click.echo(f"{prefix} Content image {i+1} generation failed: {e}") click.echo(f"{prefix} Content image {i+1} generation failed: {e}")
except Exception as e: except Exception as e:

View File

@ -6,9 +6,11 @@ import os
import re import re
import random import random
import logging import logging
import io
import requests import requests
from typing import Optional, Tuple from typing import Optional, Tuple
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from PIL import Image, ImageDraw, ImageFont
import fal_client import fal_client
from src.generation.ai_client import AIClient, PromptManager from src.generation.ai_client import AIClient, PromptManager
from src.database.repositories import ProjectRepository from src.database.repositories import ProjectRepository
@ -75,7 +77,7 @@ class ImageGenerator:
theme_prompt, _ = self.ai_client.generate_completion( theme_prompt, _ = self.ai_client.generate_completion(
prompt=user_prompt, prompt=user_prompt,
system_message=system_msg, system_message=system_msg,
max_tokens=200, max_tokens=300,
temperature=0.7 temperature=0.7
) )
@ -86,6 +88,94 @@ class ImageGenerator:
logger.info(f"Generated theme prompt for project {project_id}") logger.info(f"Generated theme prompt for project {project_id}")
return project.image_theme_prompt 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( def generate_hero_image(
self, self,
project_id: int, project_id: int,
@ -100,8 +190,7 @@ class ImageGenerator:
try: try:
theme = self.get_theme_prompt(project_id) theme = self.get_theme_prompt(project_id)
title_short = truncate_title(title, 4) prompt = theme
prompt = f"{theme} Text: '{title_short}' in clean simple uppercase letters, positioned in middle of image."
logger.info(f"Generating hero image with prompt: {prompt}") 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}") logger.info(f"Downloading hero image from URL: {image_url}")
response = requests.get(image_url, timeout=30) response = requests.get(image_url, timeout=30)
response.raise_for_status() 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}") logger.error(f"No image returned from fal.ai. Response: {result}")
return None return None

View File

@ -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"
}