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
openai==2.5.0
fal-client==0.9.1
Pillow>=10.0.0
# Testing
pytest==8.4.2
pytest-asyncio==0.21.1

View File

@ -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,

View File

@ -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:

View File

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

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