# Story 7.1: Generate and Insert Images into Articles
## Status
**PLANNING** - Ready for Implementation
## Story
**As a developer**, I want to automatically generate hero images for all article tiers and content images for T1 articles using AI-powered image generation, so that articles are visually enhanced without manual image creation.
## Context
- Epic 7 introduces AI-powered image generation to enhance article quality
- fal.ai FLUX.1 schnell API provides fast text-to-image generation (<3 seconds)
- Hero images needed for all tiers to improve visual appeal
- Content images needed for T1 articles (1-3 images) to break up text
- Images should be thematically consistent across a project
- Images are optional enhancements (failures shouldn't block article generation)
- Job JSON configuration allows per-tier customization
## Acceptance Criteria
### Core Image Generation Functionality
- Hero images generated for all tiers (if enabled in job config)
- Content images generated for T1 articles (1-3 by default, configurable)
- Images generated using fal.ai FLUX.1 schnell API
- Theme prompt generated once per project using AI, cached in database
- Images uploaded to storage zone `images/` subdirectory
- Images inserted into HTML content before saving to database
- Alt text includes 3 random entities and 2 random related searches
- Graceful failure handling (log errors, continue without images)
### Hero Image Specifications
- Default size: 1280x720 (configurable per tier in job JSON)
- Title truncated to 3-4 words, converted to UPPERCASE
- Prompt format: `{theme_prompt} Text: '{TITLE_UPPERCASE}' in clean simple uppercase letters, positioned in middle of image.`
- Naming: `{main-keyword}.jpg` (slugified)
- Placement: Inserted immediately after first `
` tag
- Format: Always JPG
- Title truncation: Extract first 3-4 words from title, convert to UPPERCASE
### Content Image Specifications
- Default size: 512x512 (configurable per tier in job JSON)
- Default count: T1 = 1-3 images (configurable min/max in job JSON)
- Prompt format: `{theme_prompt} Focus on {entity} and {related_search}, professional illustration style.`
- Naming: `{main-keyword}-{random-entity}-{random-related-search}.jpg` (all slugified)
- Placement: Distributed after H2 sections (one image per H2, evenly distributed)
- Format: Always JPG
- Alt text: `alt="entity1 related_search1 entity2 related_search2 entity3"` (3 entities, 2 related searches)
### Theme Prompt Generation
- Generated once per project using AI (OpenRouter)
- Cached in `Project.image_theme_prompt` field (TEXT, nullable)
- Prompt template: `src/generation/prompts/image_theme_generation.json`
- Reused for all images in the project for visual consistency
- Example: "Professional industrial scene, modern manufacturing equipment, clean factory environment, high-quality industrial photography, dramatic lighting."
### Job JSON Configuration
- New `image_config` section per tier in job JSON
- Structure:
```json
{
"tiers": {
"tier1": {
"image_config": {
"hero": {
"width": 1280,
"height": 720
},
"content": {
"min_num_images": 1,
"max_num_images": 3,
"width": 512,
"height": 512
}
}
}
}
}
```
- Defaults if not specified:
- Hero: 1280x720 (all tiers)
- Content: T1 = 1-3, T2/T3 = 0-0 (disabled)
- If `image_config` missing entirely: Use tier defaults
- If `image_config` present but `hero`/`content` missing: Use defaults for missing keys
### Database Schema Updates
- Add `image_theme_prompt` (TEXT, nullable) to `Project` model
- Add `hero_image_url` (TEXT, nullable) to `GeneratedContent` model
- Add `content_images` (JSON, nullable) to `GeneratedContent` model
- Format: `["url1", "url2", "url3"]` (array of image URLs)
- Migration script required
### Image Storage
- Images uploaded to `images/` subdirectory in storage zone
- Check if file exists before upload (use existing if collision)
- Return full public URL for database storage
- Handle file collisions gracefully (use existing file)
### Error Handling
- Log errors with prompt used for debugging
- Continue article generation if image generation fails
- Images are "nice to have", not required
- Max 5 concurrent fal.ai API calls (same pattern as OpenRouter)
- Retry logic similar to OpenRouter (3 attempts with backoff)
## Tasks / Subtasks
### 1. Create Image Generation Module
**Effort:** 3 story points
- [ ] Create `src/generation/image_generator.py` module
- [ ] Implement `ImageGenerator` class:
- `get_theme_prompt(project_id) -> str` (get or generate)
- `generate_hero_image(project_id, title, width, height) -> bytes`
- `generate_content_image(project_id, entity, related_search, width, height) -> bytes`
- [ ] Integrate fal.ai API client (`fal-client` package)
- [ ] Use `FAL_API_KEY` from `.env` for authentication (note: env var is `FAL_API_KEY`, not `FAL_KEY`)
- [ ] Implement concurrency control (max 5 concurrent calls)
- [ ] Implement retry logic (3 attempts with exponential backoff)
- [ ] Log errors with prompts used
### 2. Create Theme Prompt Generation
**Effort:** 2 story points
- [ ] Create `src/generation/prompts/image_theme_generation.json` prompt template
- [ ] Implement theme prompt generation using AI (OpenRouter)
- [ ] Cache theme prompt in `Project.image_theme_prompt` field
- [ ] Generate only once per project (reuse for all articles)
- [ ] Update `Project` model with `image_theme_prompt` field
- [ ] Create migration script for database schema update
### 3. Extend Job Configuration
**Effort:** 2 story points
- [ ] Update `TierConfig` in `src/generation/job_config.py`:
- Parse `image_config` from JSON
- Provide defaults if missing
- Validate min/max ranges
- [ ] Support `hero` and `content` sub-configs
- [ ] Handle missing `image_config` gracefully (use tier defaults)
### 4. Implement Image Upload & Storage
**Effort:** 2 story points
- [ ] Extend `BunnyStorageClient` or create image upload method
- [ ] Upload to `images/` subdirectory in storage zone
- [ ] Check if file exists before upload (use existing if collision)
- [ ] Generate full public URL for database storage
- [ ] Handle file naming (slugify main_keyword, entities, related_searches)
- [ ] Return URL for database storage
### 5. Implement HTML Image Insertion
**Effort:** 3 story points
- [ ] Create `src/generation/image_injection.py` module
- [ ] Implement `insert_hero_after_h1(html, hero_url, alt_text) -> str`
- [ ] Implement `insert_content_images_after_h2s(html, image_urls, alt_texts) -> str`
- [ ] Parse HTML to find H1/H2 tags
- [ ] Distribute content images evenly across H2 sections
- [ ] Generate alt text: 3 random entities + 2 random related searches
- [ ] Preserve HTML structure and formatting
### 6. Integrate into Article Generation Flow
**Effort:** 3 story points
- [ ] Update `BatchProcessor._generate_single_article()`:
- Generate hero image after title is available (all tiers)
- Generate content images after content generation (T1 only)
- Insert images into HTML before saving
- [ ] Update `BatchProcessor._generate_single_article_thread_safe()` (same changes)
- [ ] Handle tier-specific image config
- [ ] Store image URLs in database (hero_image_url, content_images JSON)
- [ ] Continue on image generation failure (log but don't block)
### 7. Database Schema Updates
**Effort:** 2 story points
- [ ] Update `Project` model: Add `image_theme_prompt` field
- [ ] Update `GeneratedContent` model:
- Add `hero_image_url` field
- Add `content_images` field (JSON)
- [ ] Create migration script `scripts/migrate_add_image_fields.py`
- [ ] Update repository interfaces if needed
### 8. Title Truncation Logic
**Effort:** 1 story point
- [ ] Implement title truncation to 3-4 words (extract first 3-4 words)
- [ ] Convert to UPPERCASE
- [ ] Handle edge cases (short titles, special characters)
- [ ] Use in hero image prompt generation
### 9. Environment Variable Setup
**Effort:** 1 story point
- [ ] Add `FAL_API_KEY` to `env.example`
- [ ] Document in README or setup guide
- [ ] Validate key exists before image generation
### 10. Unit Tests
**Effort:** 3 story points
- [ ] Test `ImageGenerator` class (mock fal.ai API)
- [ ] Test theme prompt generation and caching
- [ ] Test title truncation logic
- [ ] Test HTML image insertion
- [ ] Test job config parsing
- [ ] Test error handling and retry logic
- [ ] Achieve >80% code coverage
### 11. Integration Tests
**Effort:** 2 story points
- [ ] Test end-to-end image generation for small batch
- [ ] Test hero image generation (all tiers)
- [ ] Test content image generation (T1 only)
- [ ] Test image insertion into HTML
- [ ] Test database storage of image URLs
- [ ] Test file collision handling
- [ ] Test graceful failure (API errors)
## Technical Notes
### fal.ai API Integration
**Installation:**
```bash
pip install fal-client
```
**Environment Variable:**
```bash
FAL_API_KEY=your_fal_api_key_here
```
Note: The environment variable is `FAL_API_KEY` (not `FAL_KEY`).
**API Usage:**
```python
import fal_client
result = fal_client.subscribe(
"fal-ai/flux-1/schnell",
arguments={
"prompt": "Your prompt here",
"image_size": {"width": 1280, "height": 720},
"num_inference_steps": 4,
"guidance_scale": 3.5,
"output_format": "jpeg"
},
with_logs=True
)
# result["images"][0]["url"] contains the image URL
# Download image from URL or use data URI if sync_mode=True
```
**Concurrency:**
- Max 5 concurrent calls (same pattern as OpenRouter)
- Use ThreadPoolExecutor or similar
- Queue requests if limit exceeded
### Theme Prompt Generation
**Prompt Template:**
```json
{
"system_message": "You are an expert at creating visual style descriptions for professional images.",
"user_prompt": "Create a concise visual style description (2-3 sentences) for professional images related to: {main_keyword}\n\nEntities: {entities}\nRelated searches: {related_searches}\n\nReturn only the style description, focusing on visual elements like lighting, environment, color scheme, and overall aesthetic. This will be used as a base for all images in this project.\n\nExample format: 'Professional industrial scene, modern manufacturing equipment, clean factory environment, high-quality industrial photography, dramatic lighting.'"
}
```
**Caching:**
- Store in `Project.image_theme_prompt` field
- Generate once per project
- Reuse for all articles in project
### Image Naming Convention
**Hero Image:**
- Format: `{main-keyword}.jpg`
- Example: `shaft-machining.jpg`
- Slugify main_keyword
**Content Images:**
- Format: `{main-keyword}-{entity}-{related-search}.jpg`
- Example: `shaft-machining-cnc-lathe-precision-turning.jpg`
- Pick random entity from `project.entities`
- Pick random related_search from `project.related_searches`
- Slugify all parts
- Handle missing entities/related_searches gracefully
### HTML Insertion Logic
**Hero Image:**
- Find first `` tag in HTML
- Insert `
` tag immediately after closing `
`
- Format: `
`
**Content Images:**
- Find all `` sections
- Distribute images evenly across H2s
- If 1 image: after first H2
- If 2 images: after first and second H2
- If 3 images: after first, second, third H2
- Insert after closing `
`, before next content
**Alt Text Generation:**
- Pick 3 random entities from `project.entities`
- Pick 2 random related_searches from `project.related_searches`
- Format: `alt="entity1 related_search1 entity2 related_search2 entity3"`
- Handle missing entities/searches gracefully
### Job JSON Example
```json
{
"project_id": 1,
"tiers": {
"tier1": {
"count": 5,
"image_config": {
"hero": {
"width": 1280,
"height": 720
},
"content": {
"min_num_images": 1,
"max_num_images": 3,
"width": 512,
"height": 512
}
}
},
"tier2": {
"count": 20,
"image_config": {
"hero": {
"width": 1280,
"height": 720
}
}
}
}
}
```
### Database Schema Updates
```sql
-- Add theme prompt to projects
ALTER TABLE projects ADD COLUMN image_theme_prompt TEXT NULL;
-- Add image fields to generated_content
ALTER TABLE generated_content ADD COLUMN hero_image_url TEXT NULL;
ALTER TABLE generated_content ADD COLUMN content_images JSON NULL;
```
## Dependencies
- fal.ai API account and `FAL_API_KEY`
- `fal-client` Python package
- OpenRouter API for theme prompt generation
- Bunny.net storage for image hosting
- Story 4.1: Deployment infrastructure (for image uploads)
## Future Considerations
- Image optimization (compression, WebP conversion)
- CDN caching for images
- Image format conversion (PNG, WebP options)
- Custom image styles per project
- Image regeneration on content updates
- Image analytics (views, engagement)
## Technical Debt Created
- Image optimization deferred (current: full-size JPGs)
- No CDN cache purging for images
- No image format conversion (always JPG)
- Theme prompt regeneration not implemented (manual update required)
- Image deletion not implemented (orphaned images may accumulate)
## Total Effort
24 story points
### Effort Breakdown
1. Image Generation Module (3 points)
2. Theme Prompt Generation (2 points)
3. Job Configuration Extension (2 points)
4. Image Upload & Storage (2 points)
5. HTML Image Insertion (3 points)
6. Article Generation Integration (3 points)
7. Database Schema Updates (2 points)
8. Title Truncation Logic (1 point)
9. Environment Variable Setup (1 point)
10. Unit Tests (3 points)
11. Integration Tests (2 points)
## Questions & Clarifications
### Question 1: Image Format
**Status:** ✓ RESOLVED
Decision: Always JPG
- Consistent format across all images
- Good balance of quality and file size
- Future: Could add format options in job config
### Question 2: Hero Image Title Truncation
**Status:** ✓ RESOLVED
Decision: Truncate to 3-4 words, UPPERCASE
- Prevents overly long text in images
- Improves readability
- Example: "Overview of Shaft Machining" → "OVERVIEW OF SHAFT MACHINING"
### Question 3: Content Image Count
**Status:** ✓ RESOLVED
Decision: T1 default 1-3 images, configurable per tier
- Job JSON allows fine-grained control
- Defaults: T1 = 1-3, T2/T3 = 0-0 (disabled)
- Can override in job config
### Question 4: Image Failure Handling
**Status:** ✓ RESOLVED
Decision: Log errors, continue without images
- Images are enhancements, not required
- Article generation continues even if images fail
- Log includes prompt used for debugging
## Notes
- Images enhance SEO and user engagement
- fal.ai provides fast generation (<3 seconds per image)
- Theme consistency ensures visual coherence across project
- Job JSON provides flexibility for different use cases
- Graceful degradation if image generation fails
- All API keys from `.env` file only