Files
wp-agentic-writer/docs/implementation/IMAGE_GENERATION_IMPLEMENTATION_PLAN.md

1390 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WP Agentic Writer: Image Generation Implementation Plan
**Document Version:** 1.1
**Date:** January 28, 2026
**Status:** Ready for Implementation
**Estimated Time:** 16-20 hours
**Changelog v1.1:**
- Updated table names to use `wp_wpaw_` prefix (consistent with existing tables)
- Changed temp folder location to `/wp-content/uploads/wpaw/{post_id}/`
- Added user-controlled variant count setting (1-3 variants per image)
---
## Executive Summary
This document provides a comprehensive implementation plan for adding AI-powered image generation to WP Agentic Writer. The implementation follows **Option A (Cost-Optimized with User Control)** from the recommendations, ensuring maximum cost efficiency and quality control.
### Key Features
-**Cost-optimized flow:** Analysis + prompts cost ~$0.002, user controls image generation
-**Model-specific prompts:** Tailored for FLUX.2 klein (Budget), Riverflow V2 Max (Balanced), FLUX.2 max (Premium)
-**Variant management:** User controls variant count (1-3), selects best one
-**WordPress integration:** Direct upload to Media Library with alt text
-**Temp file management:** Automatic cleanup of unused variants
---
## Table of Contents
1. [Current State Analysis](#current-state-analysis)
2. [Architecture Overview](#architecture-overview)
3. [Database Schema](#database-schema)
4. [Implementation Phases](#implementation-phases)
5. [Phase 1: Database & Core Infrastructure](#phase-1-database--core-infrastructure)
6. [Phase 2: Backend Image Generation](#phase-2-backend-image-generation)
7. [Phase 3: Frontend UI & Modal](#phase-3-frontend-ui--modal)
8. [Phase 4: Gutenberg Integration](#phase-4-gutenberg-integration)
9. [Phase 5: Temp File Management](#phase-5-temp-file-management)
10. [Phase 6: Testing & Polish](#phase-6-testing--polish)
11. [Configuration & Settings](#configuration--settings)
12. [Cost Tracking Integration](#cost-tracking-integration)
13. [Security Considerations](#security-considerations)
14. [Rollout Strategy](#rollout-strategy)
---
## Current State Analysis
### ✅ What We Have
1. **Image Model Configuration**
- Settings page has image model selector
- Default: `openai/gpt-4o`
- Preset support: Budget/Balanced/Premium all use GPT-4o
- Model stored in `wp_agentic_writer_settings['image_model']`
2. **Image Placeholder Support**
- Writing agent can suggest images using `[IMAGE: description]` format
- Markdown parser converts `[IMAGE: ...]` to `core/image` blocks
- Post config has `include_images` boolean flag
3. **OpenRouter Provider**
- Has `generate_image()` method (basic implementation)
- Uses chat completion for images (not optimal)
- No variant support, no temp file management
4. **Cost Tracking**
- Existing table: `wp_wpaw_cost_tracking`
- Tracks model, action, tokens, cost per request
- Can be extended for image generation costs
### ❌ What We Need
1. **Database Tables**
- `wp_wpaw_images` - Image recommendations and metadata
- `wp_wpaw_images_variants` - Generated image variants
2. **Image Generation Flow**
- Analyze article for placement
- Generate model-specific prompts
- Generate image variants via OpenRouter
- Download and store temp files
- Upload selected variant to WordPress Media
3. **Frontend UI**
- Image review modal after article generation
- Variant selection interface
- Gutenberg block toolbar integration
- Progress indicators
4. **File Management**
- Temp directory: `/wp-content/uploads/wpaw/{post_id}/`
- Automatic cleanup (7+ days)
- Cron job for maintenance
5. **User Controls**
- Variant count selector (1-3 variants per image)
- Cost preview before generation
- Per-image generation control
6. **Model-Specific Prompting**
- FLUX.2 klein: Simple 1-2 sentence prompts
- Riverflow V2 Max: Detailed 3-4 sentence prompts
- FLUX.2 max: Complex 4-6 sentence prompts
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ ARTICLE GENERATION COMPLETE │
│ (Writing agent finishes, returns markdown with [IMAGE: ...])│
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ AUTOMATIC: Analyze & Generate Prompts │
│ • analyze_article_for_images() → placement points │
│ • generate_image_prompts() → 3 image specs │
│ • Store in wp_wpaw_images table │
│ Cost: ~$0.002 (uses writing model) │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND: Image Review Modal │
│ • Show 3 image recommendations │
│ • Display prompts (editable) │
│ • Display alt text (editable) │
│ • Variant count selector (1-3 per image) │
│ • Show cost estimate per image × variant count │
│ • [Generate All] [Generate Selected] [Skip] │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ USER SELECTS: Generate Image(s) │
│ • Choose variant count (1-3) per image │
│ • Click [Generate] on individual image │
│ • OR click [Generate All] │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ BACKEND: Generate Variants │
│ • Call OpenRouter image generation API │
│ • Generate N variants (user-specified: 1-3) │
│ • Download to /wp-content/uploads/wpaw/{post_id}/ │
│ • Store in wp_wpaw_images_variants table │
│ • Return variant URLs to frontend │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND: Variant Selection Modal │
│ • Display all variants in grid │
│ • Show generation info (cost, time, model) │
│ • [Select] button per variant │
│ • [Regenerate] for more variants │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ BACKEND: Commit to WordPress Media │
│ • media_handle_sideload() - upload to Media Library │
│ • Set alt text from recommendation │
│ • Update wp_wpaw_images: status='committed' │
│ • Return attachment ID + URL │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND: Update Gutenberg Block │
│ • updateBlockAttributes() with attachment ID/URL │
│ • Remove data-agent-image-id placeholder marker │
│ • Image now permanent in WordPress │
└─────────────────────────────────────────────────────────────┘
```
---
## Database Schema
### Table 1: `wp_wpaw_images`
Stores image recommendations from the writing agent.
```sql
CREATE TABLE IF NOT EXISTS `wp_wpaw_images` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`post_id` bigint(20) NOT NULL,
-- Recommendation from agent
`agent_image_id` varchar(50) NOT NULL, -- e.g., "img_hero_1"
`placement` varchar(100) DEFAULT NULL, -- "intro_hero", "after_section_2"
`section_title` varchar(255) DEFAULT NULL, -- "Introduction to n8n"
-- Original recommendation
`prompt_initial` text NOT NULL, -- Agent's initial prompt
`alt_text_initial` text DEFAULT NULL, -- Agent's suggested alt
-- User edits (nullable)
`prompt_edited` text DEFAULT NULL, -- NULL if user didn't edit
`alt_text_edited` text DEFAULT NULL, -- NULL if user didn't edit
-- Committed image (when user selects a variant)
`attachment_id` bigint(20) DEFAULT NULL, -- WP attachment ID (null until committed)
`status` varchar(30) DEFAULT 'pending', -- pending, generating, committed, discarded
-- Cost tracking
`cost_estimate` decimal(10, 4) DEFAULT NULL, -- Based on image model pricing
`cost_actual` decimal(10, 4) DEFAULT NULL, -- Updated after generation
`image_model` varchar(100) DEFAULT NULL, -- Which model was used
-- Metadata
`created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_post` (`post_id`),
KEY `idx_agent_image_id` (`post_id`, `agent_image_id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### Table 2: `wp_wpaw_images_variants`
Tracks all generated variants (temp images) for each agent_image_id.
```sql
CREATE TABLE IF NOT EXISTS `wp_wpaw_images_variants` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
-- Reference to main image record
`agentic_image_id` bigint(20) NOT NULL,
`post_id` bigint(20) NOT NULL,
`agent_image_id` varchar(50) NOT NULL, -- e.g., "img_hero_1"
-- Variant details
`variant_number` int(11) DEFAULT 1, -- 1st, 2nd, 3rd generation attempt
`temp_file_path` varchar(500) NOT NULL, -- /wp-content/uploads/wpaw/{post_id}/xxx.jpg
`temp_file_url` varchar(500) NOT NULL, -- URL to temp image
`file_size` int(11) DEFAULT NULL, -- In bytes
-- Generation details
`prompt_used` text DEFAULT NULL, -- Exact prompt sent to image model
`image_model_used` varchar(100) DEFAULT NULL, -- Which model generated this
`generation_time` int(11) DEFAULT NULL, -- Seconds to generate
`cost` decimal(10, 4) DEFAULT NULL, -- Cost of this generation
-- Selection status
`is_selected` tinyint(1) DEFAULT 0, -- 1 if user selected this variant
`selected_at` datetime DEFAULT NULL,
-- Lifecycle
`status` varchar(30) DEFAULT 'temp', -- temp, selected, discarded, auto_deleted
`created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_agentic_image` (`agentic_image_id`),
KEY `idx_post` (`post_id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
---
## Implementation Phases
### Phase 1: Database & Core Infrastructure (2-3 hours)
- Create database tables
- Create temp directory structure
- Add activation/deactivation hooks
### Phase 2: Backend Image Generation (4-5 hours)
- Implement image analysis
- Implement prompt generation (model-specific)
- Implement OpenRouter image generation
- Implement variant management
- Implement Media Library upload
### Phase 3: Frontend UI & Modal (4-5 hours)
- Image review modal
- Variant selection interface
- Progress indicators
- Error handling
### Phase 4: Gutenberg Integration (2-3 hours)
- Block toolbar button
- Block attribute updates
- Placeholder management
### Phase 5: Temp File Management (2 hours)
- Cleanup cron job
- Admin page for temp images
- Manual cleanup interface
### Phase 6: Testing & Polish (2-3 hours)
- End-to-end testing
- Error scenarios
- Performance optimization
- Documentation
**Total Estimated Time:** 16-21 hours
---
## Phase 1: Database & Core Infrastructure
### 1.1 Create Database Tables
**File:** `includes/class-image-manager.php` (NEW)
```php
<?php
/**
* Image Manager Class
*
* Handles image generation, variant management, and WordPress Media integration.
*
* @package WP_Agentic_Writer
*/
class WP_Agentic_Writer_Image_Manager {
/**
* Singleton instance.
*/
private static $instance = null;
/**
* Get singleton instance.
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
// Register activation hook
register_activation_hook( WP_AGENTIC_WRITER_FILE, array( $this, 'create_tables' ) );
}
/**
* Create database tables on plugin activation.
*/
public function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
// Table 1: wp_wpaw_images
$table_images = $wpdb->prefix . 'wpaw_images';
$sql_images = "CREATE TABLE IF NOT EXISTS `{$table_images}` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`post_id` bigint(20) NOT NULL,
`agent_image_id` varchar(50) NOT NULL,
`placement` varchar(100) DEFAULT NULL,
`section_title` varchar(255) DEFAULT NULL,
`prompt_initial` text NOT NULL,
`alt_text_initial` text DEFAULT NULL,
`prompt_edited` text DEFAULT NULL,
`alt_text_edited` text DEFAULT NULL,
`attachment_id` bigint(20) DEFAULT NULL,
`status` varchar(30) DEFAULT 'pending',
`cost_estimate` decimal(10, 4) DEFAULT NULL,
`cost_actual` decimal(10, 4) DEFAULT NULL,
`image_model` varchar(100) DEFAULT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_post` (`post_id`),
KEY `idx_agent_image_id` (`post_id`, `agent_image_id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) {$charset_collate};";
// Table 2: wp_wpaw_images_variants
$table_variants = $wpdb->prefix . 'wpaw_images_variants';
$sql_variants = "CREATE TABLE IF NOT EXISTS `{$table_variants}` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`agentic_image_id` bigint(20) NOT NULL,
`post_id` bigint(20) NOT NULL,
`agent_image_id` varchar(50) NOT NULL,
`variant_number` int(11) DEFAULT 1,
`temp_file_path` varchar(500) NOT NULL,
`temp_file_url` varchar(500) NOT NULL,
`file_size` int(11) DEFAULT NULL,
`prompt_used` text DEFAULT NULL,
`image_model_used` varchar(100) DEFAULT NULL,
`generation_time` int(11) DEFAULT NULL,
`cost` decimal(10, 4) DEFAULT NULL,
`is_selected` tinyint(1) DEFAULT 0,
`selected_at` datetime DEFAULT NULL,
`status` varchar(30) DEFAULT 'temp',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_agentic_image` (`agentic_image_id`),
KEY `idx_post` (`post_id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) {$charset_collate};";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql_images );
dbDelta( $sql_variants );
// Create temp directory
$this->create_temp_directory();
}
/**
* Create temp directory for image storage.
*/
private function create_temp_directory() {
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/wpaw';
if ( ! file_exists( $temp_dir ) ) {
wp_mkdir_p( $temp_dir );
// Add .htaccess to prevent direct access
$htaccess = $temp_dir . '/.htaccess';
if ( ! file_exists( $htaccess ) ) {
file_put_contents( $htaccess, "Options -Indexes\n" );
}
// Add index.php for security
$index = $temp_dir . '/index.php';
if ( ! file_exists( $index ) ) {
file_put_contents( $index, "<?php // Silence is golden\n" );
}
}
}
}
```
### 1.2 Update Plugin Activation
**File:** `wp-agentic-writer.php`
```php
// Add to plugin initialization
function wp_agentic_writer_activate() {
// Existing activation code...
// Initialize image manager (creates tables)
WP_Agentic_Writer_Image_Manager::get_instance()->create_tables();
}
register_activation_hook( __FILE__, 'wp_agentic_writer_activate' );
```
### 1.3 Update Autoloader
**File:** `includes/class-autoloader.php`
```php
// Add to class map
'WP_Agentic_Writer_Image_Manager' => 'class-image-manager.php',
```
---
## Phase 2: Backend Image Generation
### 2.1 Analyze Article for Image Placement
**File:** `includes/class-image-manager.php` (add methods)
```php
/**
* Analyze article for optimal image placement.
*
* @param string $article_markdown Article content in markdown.
* @param int $post_id Post ID.
* @return array|WP_Error Placement data or error.
*/
public function analyze_article_for_images( $article_markdown, $post_id ) {
$settings = get_option( 'wp_agentic_writer_settings', array() );
$writing_model = $settings['writing_model'] ?? 'anthropic/claude-3.5-sonnet';
$system_prompt = "You are an expert content strategist analyzing articles for optimal image placement.
Your task: Identify 2-3 strategic locations where images would enhance understanding and engagement.
RULES:
1. Prioritize placement after introduction (hero image)
2. Consider complex sections that need visual aids
3. Look for opportunities before conclusions
4. Maximum 3 images per article
Return JSON:
{
\"recommended_image_count\": 3,
\"image_placement_points\": [
{
\"agent_image_id\": \"img_hero_1\",
\"placement\": \"after_introduction\",
\"section_title\": \"Introduction\",
\"image_type\": \"hero_dashboard\",
\"reasoning\": \"Sets visual tone for article\"
}
]
}";
$messages = array(
array(
'role' => 'user',
'content' => "Analyze this article for image placement:\n\n" . $article_markdown,
),
);
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$response = $provider->chat( $messages, array( 'temperature' => 0.3 ), 'planning' );
if ( is_wp_error( $response ) ) {
return $response;
}
// Extract JSON from response
$json_match = array();
if ( preg_match( '/\{[\s\S]*\}/m', $response['content'], $json_match ) ) {
$placement_data = json_decode( $json_match[0], true );
if ( json_last_error() === JSON_ERROR_NONE ) {
return $placement_data;
}
}
return new WP_Error( 'parse_error', 'Failed to parse placement analysis' );
}
```
### 2.2 Generate Model-Specific Prompts
```php
/**
* Generate image prompts optimized for specific image model.
*
* @param string $article_markdown Article content.
* @param array $placement_data Placement analysis.
* @param int $post_id Post ID.
* @return array|WP_Error Image specifications or error.
*/
public function generate_image_prompts( $article_markdown, $placement_data, $post_id ) {
$settings = get_option( 'wp_agentic_writer_settings', array() );
$writing_model = $settings['writing_model'] ?? 'anthropic/claude-3.5-sonnet';
$image_model = $settings['image_model'] ?? 'openai/gpt-4o';
// Get model-specific prompt guidance
$prompt_guidance = $this->get_prompt_guidance_for_model( $image_model );
$system_prompt = "You are an Image Prompt Engineer specializing in {$prompt_guidance['model_name']}.
TARGET MODEL: {$prompt_guidance['model_name']}
PROMPT LENGTH: {$prompt_guidance['prompt_length']}
COMPLEXITY: {$prompt_guidance['complexity']}
{$prompt_guidance['guidance']}
TEMPLATE: {$prompt_guidance['template']}
Generate precise, cost-efficient prompts that exploit this model's strengths.
Return JSON:
{
\"images\": [
{
\"agent_image_id\": \"img_hero_1\",
\"placement\": \"after_introduction\",
\"section_title\": \"Introduction\",
\"prompt\": \"[Model-optimized prompt]\",
\"alt\": \"Descriptive alt text\",
\"image_model\": \"{$image_model}\"
}
]
}";
$user_input = json_encode( array(
'article' => $article_markdown,
'placement_points' => $placement_data['image_placement_points'],
'image_count' => $placement_data['recommended_image_count'],
'target_image_model' => $image_model,
) );
$messages = array(
array(
'role' => 'user',
'content' => "Generate image prompts:\n\n" . $user_input,
),
);
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$response = $provider->chat( $messages, array( 'temperature' => 0.7 ), 'planning' );
if ( is_wp_error( $response ) ) {
return $response;
}
// Extract JSON
$json_match = array();
if ( preg_match( '/\{[\s\S]*\}/m', $response['content'], $json_match ) ) {
$image_specs = json_decode( $json_match[0], true );
if ( json_last_error() === JSON_ERROR_NONE ) {
// Save to database
$this->save_image_recommendations( $post_id, $image_specs['images'] );
return $image_specs;
}
}
return new WP_Error( 'parse_error', 'Failed to parse image prompts' );
}
/**
* Get prompt guidance for specific image model.
*/
private function get_prompt_guidance_for_model( $image_model ) {
$model_configs = array(
'black-forest-labs/flux.2-klein' => array(
'model_name' => 'FLUX.2 [klein]',
'prompt_length' => '1-2 sentences',
'complexity' => 'simple',
'guidance' => 'Keep prompts short and simple. Focus on main subject, key details, and style. Avoid complex scenes or technical specifications.',
'template' => 'Subject, key elements, style, color palette',
),
'sourceful/riverflow-v2-max' => array(
'model_name' => 'Riverflow V2 Max',
'prompt_length' => '3-4 sentences',
'complexity' => 'medium-detailed',
'guidance' => 'Include context, environment details, lighting style, and photographic specifications. Model excels at photorealism.',
'template' => 'Subject + context, environment details, lighting style, photography style, technical specs',
),
'black-forest-labs/flux.2-max' => array(
'model_name' => 'FLUX.2 [max]',
'prompt_length' => '4-6 sentences',
'complexity' => 'very-detailed-technical',
'guidance' => 'Use detailed technical vocabulary. Include exact materials, color codes (HEX), spatial relationships, and specifications.',
'template' => 'Technical foundation, main subject + action, environment, lighting + mood, style + aesthetics, technical specifications',
),
);
// Default to Riverflow if model not found
return $model_configs[ $image_model ] ?? $model_configs['sourceful/riverflow-v2-max'];
}
/**
* Save image recommendations to database.
*/
private function save_image_recommendations( $post_id, $images ) {
global $wpdb;
$table = $wpdb->prefix . 'wpaw_images';
foreach ( $images as $image_spec ) {
$wpdb->insert(
$table,
array(
'post_id' => $post_id,
'agent_image_id' => $image_spec['agent_image_id'],
'placement' => $image_spec['placement'],
'section_title' => $image_spec['section_title'],
'prompt_initial' => $image_spec['prompt'],
'alt_text_initial' => $image_spec['alt'],
'image_model' => $image_spec['image_model'],
'status' => 'pending',
),
array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
);
}
}
```
### 2.3 Generate Image via OpenRouter
**File:** `includes/class-openrouter-provider.php` (update existing method)
```php
/**
* Generate image using OpenRouter image generation API.
*
* @param string $prompt Image prompt.
* @param string $model Image model (optional, uses default if not provided).
* @param array $options Additional options (size, quality, etc).
* @return array|WP_Error Response with image URL or error.
*/
public function generate_image( $prompt, $model = null, $options = array() ) {
if ( empty( $this->api_key ) ) {
return new WP_Error( 'no_api_key', 'OpenRouter API key not configured' );
}
$model = $model ?? $this->image_model;
$size = $options['size'] ?? '1024x576'; // Default blog size
$quality = $options['quality'] ?? 'hd';
$n = $options['n'] ?? 1; // Number of variants
$start_time = microtime( true );
$response = wp_remote_post(
'https://openrouter.ai/api/v1/images/generations',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
'HTTP-Referer' => home_url(),
'X-Title' => get_bloginfo( 'name' ),
),
'body' => wp_json_encode( array(
'model' => $model,
'prompt' => $prompt,
'n' => $n,
'size' => $size,
'quality' => $quality,
) ),
'timeout' => 60,
)
);
$generation_time = microtime( true ) - $start_time;
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! isset( $body['data'][0]['url'] ) ) {
return new WP_Error(
'image_generation_failed',
$body['error']['message'] ?? 'Unknown error'
);
}
return array(
'url' => $body['data'][0]['url'],
'cost' => $body['usage']['cost'] ?? 0.03, // Fallback estimate
'generation_time' => $generation_time,
'model' => $model,
);
}
```
### 2.4 REST API Endpoints
**File:** `includes/class-gutenberg-sidebar.php` (add to `register_routes()`)
```php
// Image generation endpoints
register_rest_route(
'wp-agentic-writer/v1',
'/analyze-images',
array(
'methods' => 'POST',
'callback' => array( $this, 'handle_analyze_images' ),
'permission_callback' => array( $this, 'check_permissions' ),
)
);
register_rest_route(
'wp-agentic-writer/v1',
'/generate-image',
array(
'methods' => 'POST',
'callback' => array( $this, 'handle_generate_image' ),
'permission_callback' => array( $this, 'check_permissions' ),
)
);
register_rest_route(
'wp-agentic-writer/v1',
'/commit-image',
array(
'methods' => 'POST',
'callback' => array( $this, 'handle_commit_image' ),
'permission_callback' => array( $this, 'check_permissions' ),
)
);
register_rest_route(
'wp-agentic-writer/v1',
'/image-recommendations/(?P<post_id>\d+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'handle_get_image_recommendations' ),
'permission_callback' => array( $this, 'check_permissions' ),
)
);
```
---
## Phase 3: Frontend UI & Modal
### 3.1 Image Review Modal Component
**File:** `assets/js/image-modal.js` (NEW)
```javascript
/**
* Image Generation Modal Component
*
* Handles image review, generation, variant selection, and commitment.
*/
(function() {
const { Modal, Button, Spinner, TextControl, TextareaControl } = wp.components;
const { useState, useEffect } = wp.element;
window.wpAgenticWriter = window.wpAgenticWriter || {};
/**
* Image Review Modal
* Shows after article generation with image recommendations
*/
window.wpAgenticWriter.ImageReviewModal = function({ postId, onClose, onComplete }) {
const [step, setStep] = useState('loading'); // loading, review, generating, selecting
const [images, setImages] = useState([]);
const [selectedImages, setSelectedImages] = useState([]);
const [variantCounts, setVariantCounts] = useState({}); // Track variant count per image
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState(null);
// Load image recommendations
useEffect(() => {
loadImageRecommendations();
}, []);
const loadImageRecommendations = async () => {
try {
const response = await fetch(
`${wpAgenticWriter.apiUrl}/image-recommendations/${postId}`,
{
headers: {
'X-WP-Nonce': wpAgenticWriter.nonce,
},
}
);
if (!response.ok) {
throw new Error('Failed to load image recommendations');
}
const data = await response.json();
const imgs = data.images || [];
setImages(imgs);
// Initialize variant counts to 2 for each image
const initialCounts = {};
imgs.forEach(img => {
initialCounts[img.agent_image_id] = 2; // Default: 2 variants
});
setVariantCounts(initialCounts);
setStep('review');
} catch (err) {
setError(err.message);
setStep('review');
}
};
const handleEditPrompt = (imageId, newPrompt) => {
setImages(prev => prev.map(img =>
img.agent_image_id === imageId
? { ...img, prompt_edited: newPrompt }
: img
));
};
const handleEditAlt = (imageId, newAlt) => {
setImages(prev => prev.map(img =>
img.agent_image_id === imageId
? { ...img, alt_text_edited: newAlt }
: img
));
};
const handleVariantCountChange = (imageId, count) => {
setVariantCounts(prev => ({
...prev,
[imageId]: parseInt(count, 10)
}));
};
const calculateTotalCost = () => {
const settings = wpAgenticWriter.settings || {};
const imageModel = settings.image_model || 'sourceful/riverflow-v2-max';
// Cost per image by model (estimates)
const costPerImage = {
'black-forest-labs/flux.2-klein': 0.02,
'sourceful/riverflow-v2-max': 0.03,
'black-forest-labs/flux.2-max': 0.15,
};
const baseCost = costPerImage[imageModel] || 0.03;
let total = 0;
selectedImages.forEach(imageId => {
const count = variantCounts[imageId] || 1;
total += baseCost * count;
});
return total.toFixed(3);
};
const handleGenerateSelected = async () => {
if (selectedImages.length === 0) {
alert('Please select at least one image to generate');
return;
}
setIsGenerating(true);
setStep('generating');
try {
for (const imageId of selectedImages) {
const image = images.find(img => img.agent_image_id === imageId);
const response = await fetch(
`${wpAgenticWriter.apiUrl}/generate-image`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpAgenticWriter.nonce,
},
body: JSON.stringify({
post_id: postId,
agent_image_id: imageId,
prompt: image.prompt_edited || image.prompt_initial,
alt: image.alt_text_edited || image.alt_text_initial,
variant_count: variantCounts[imageId] || 1,
}),
}
);
if (!response.ok) {
throw new Error(`Failed to generate image: ${imageId}`);
}
const result = await response.json();
// Update image with variants
setImages(prev => prev.map(img =>
img.agent_image_id === imageId
? { ...img, variants: result.variants }
: img
));
}
setStep('selecting');
} catch (err) {
setError(err.message);
setStep('review');
} finally {
setIsGenerating(false);
}
};
const handleSelectVariant = async (imageId, variantId) => {
const image = images.find(img => img.agent_image_id === imageId);
try {
const response = await fetch(
`${wpAgenticWriter.apiUrl}/commit-image`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpAgenticWriter.nonce,
},
body: JSON.stringify({
post_id: postId,
agent_image_id: imageId,
variant_id: variantId,
alt: image.alt_text_edited || image.alt_text_initial,
}),
}
);
if (!response.ok) {
throw new Error('Failed to commit image');
}
const result = await response.json();
// Update Gutenberg block
updateGutenbergBlock(imageId, result);
// Mark as committed
setImages(prev => prev.map(img =>
img.agent_image_id === imageId
? { ...img, status: 'committed', attachment_id: result.attachment_id }
: img
));
} catch (err) {
alert('Failed to commit image: ' + err.message);
}
};
const updateGutenbergBlock = (agentImageId, attachmentData) => {
// Find block with matching data-agent-image-id
const blocks = wp.data.select('core/block-editor').getBlocks();
const findAndUpdateBlock = (blocks) => {
for (const block of blocks) {
if (block.name === 'core/image' &&
block.attributes['data-agent-image-id'] === agentImageId) {
wp.data.dispatch('core/block-editor').updateBlockAttributes(
block.clientId,
{
id: attachmentData.attachment_id,
url: attachmentData.attachment_url,
alt: attachmentData.alt,
'data-agent-image-id': undefined, // Remove placeholder marker
}
);
return true;
}
if (block.innerBlocks && block.innerBlocks.length > 0) {
if (findAndUpdateBlock(block.innerBlocks)) {
return true;
}
}
}
return false;
};
findAndUpdateBlock(blocks);
};
// Render functions for each step
if (step === 'loading') {
return wp.element.createElement(Modal, {
title: 'Loading Image Recommendations',
onRequestClose: onClose,
},
wp.element.createElement('div', { style: { padding: '20px', textAlign: 'center' } },
wp.element.createElement(Spinner)
)
);
}
if (step === 'review') {
return wp.element.createElement(Modal, {
title: `Image Recommendations (${images.length})`,
onRequestClose: onClose,
style: { maxWidth: '800px' },
},
wp.element.createElement('div', { className: 'wpaw-image-review' },
error && wp.element.createElement('div', {
className: 'notice notice-error',
style: { marginBottom: '20px' }
}, error),
images.map(image =>
wp.element.createElement('div', {
key: image.agent_image_id,
className: 'wpaw-image-card',
style: {
border: '1px solid #ddd',
padding: '15px',
marginBottom: '15px',
borderRadius: '4px',
},
},
wp.element.createElement('h3', null,
`Image: ${image.section_title || image.placement}`
),
wp.element.createElement(TextareaControl, {
label: 'Prompt',
value: image.prompt_edited || image.prompt_initial,
onChange: (value) => handleEditPrompt(image.agent_image_id, value),
rows: 3,
}),
wp.element.createElement(TextControl, {
label: 'Alt Text',
value: image.alt_text_edited || image.alt_text_initial,
onChange: (value) => handleEditAlt(image.agent_image_id, value),
}),
wp.element.createElement('div', {
style: { marginTop: '10px', marginBottom: '10px' }
},
wp.element.createElement('label', {
style: { display: 'block', marginBottom: '5px', fontWeight: '600' }
}, 'Variant Count'),
wp.element.createElement('select', {
value: variantCounts[image.agent_image_id] || 2,
onChange: (e) => handleVariantCountChange(image.agent_image_id, e.target.value),
style: {
padding: '5px',
borderRadius: '3px',
border: '1px solid #ddd',
}
},
wp.element.createElement('option', { value: '1' }, '1 variant'),
wp.element.createElement('option', { value: '2' }, '2 variants'),
wp.element.createElement('option', { value: '3' }, '3 variants')
),
wp.element.createElement('p', {
style: { fontSize: '12px', color: '#666', margin: '5px 0 0' }
}, `Cost: ~$${((variantCounts[image.agent_image_id] || 2) * 0.03).toFixed(3)}`)
),
wp.element.createElement('label', null,
wp.element.createElement('input', {
type: 'checkbox',
checked: selectedImages.includes(image.agent_image_id),
onChange: (e) => {
if (e.target.checked) {
setSelectedImages(prev => [...prev, image.agent_image_id]);
} else {
setSelectedImages(prev => prev.filter(id => id !== image.agent_image_id));
}
},
}),
' Generate this image'
)
)
),
wp.element.createElement('div', {
style: {
marginTop: '20px',
display: 'flex',
gap: '10px',
justifyContent: 'flex-end',
}
},
wp.element.createElement(Button, {
variant: 'secondary',
onClick: onClose,
}, 'Skip Images'),
wp.element.createElement(Button, {
variant: 'primary',
onClick: handleGenerateSelected,
disabled: selectedImages.length === 0 || isGenerating,
}, `Generate ${selectedImages.length} Image(s) (~$${calculateTotalCost()})`)
)
)
);
}
if (step === 'generating') {
return wp.element.createElement(Modal, {
title: 'Generating Images',
onRequestClose: () => {},
},
wp.element.createElement('div', { style: { padding: '20px', textAlign: 'center' } },
wp.element.createElement(Spinner),
wp.element.createElement('p', null,
`Generating images... This may take a minute.`
),
wp.element.createElement('p', { style: { fontSize: '12px', color: '#666' } },
`Estimated cost: $${calculateTotalCost()}`
)
)
);
}
if (step === 'selecting') {
return wp.element.createElement(Modal, {
title: 'Select Image Variants',
onRequestClose: onClose,
style: { maxWidth: '900px' },
},
wp.element.createElement('div', { className: 'wpaw-variant-selection' },
images
.filter(img => img.variants && img.variants.length > 0)
.map(image =>
wp.element.createElement('div', {
key: image.agent_image_id,
style: { marginBottom: '30px' },
},
wp.element.createElement('h3', null, image.section_title),
wp.element.createElement('div', {
style: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '15px',
},
},
image.variants.map(variant =>
wp.element.createElement('div', {
key: variant.id,
style: {
border: '1px solid #ddd',
borderRadius: '4px',
overflow: 'hidden',
},
},
wp.element.createElement('img', {
src: variant.temp_file_url,
alt: 'Variant',
style: { width: '100%', display: 'block' },
}),
wp.element.createElement('div', { style: { padding: '10px' } },
wp.element.createElement('p', { style: { fontSize: '12px', margin: '0 0 10px' } },
`Cost: $${variant.cost.toFixed(3)}${variant.generation_time}s`
),
wp.element.createElement(Button, {
variant: 'primary',
onClick: () => handleSelectVariant(image.agent_image_id, variant.id),
style: { width: '100%' },
}, 'Select')
)
)
)
)
)
),
wp.element.createElement('div', {
style: { marginTop: '20px', textAlign: 'right' },
},
wp.element.createElement(Button, {
variant: 'secondary',
onClick: onComplete,
}, 'Done')
)
)
);
}
};
})();
```
---
## Configuration & Settings
### Update Model Presets
**File:** `includes/class-settings.php` and `includes/class-settings-v2.php`
Update presets to use recommended image models:
```php
$presets = array(
'budget' => array(
'chat' => 'google/gemini-2.5-flash',
'clarity' => 'google/gemini-2.5-flash',
'planning' => 'google/gemini-2.5-flash',
'writing' => 'mistralai/mistral-small-creative',
'refinement' => 'google/gemini-2.5-flash',
'image' => 'black-forest-labs/flux.2-klein', // UPDATED
),
'balanced' => array(
'chat' => 'google/gemini-2.5-flash',
'clarity' => 'google/gemini-2.5-flash',
'planning' => 'google/gemini-2.5-flash',
'writing' => 'anthropic/claude-3.5-sonnet',
'refinement' => 'anthropic/claude-3.5-sonnet',
'image' => 'sourceful/riverflow-v2-max', // UPDATED
),
'premium' => array(
'chat' => 'google/gemini-3-flash-preview',
'clarity' => 'anthropic/claude-sonnet-4',
'planning' => 'google/gemini-3-flash-preview',
'writing' => 'openai/gpt-4.1',
'refinement' => 'openai/gpt-4.1',
'image' => 'black-forest-labs/flux.2-max', // UPDATED
),
);
```
---
## Cost Tracking Integration
Update cost tracking to include image generation:
```php
// In class-cost-tracker.php
public function track_image_generation( $post_id, $image_model, $cost, $generation_time ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'wpaw_cost_tracking',
array(
'post_id' => $post_id,
'model' => $image_model,
'action' => 'image_generation',
'input_tokens' => 0, // Images don't use tokens
'output_tokens' => 0,
'cost' => $cost,
),
array( '%d', '%s', '%s', '%d', '%d', '%f' )
);
}
```
---
## Security Considerations
1. **File Upload Security**
- Validate image file types (JPEG, PNG only)
- Sanitize filenames
- Check file size limits
- Use WordPress media_handle_sideload()
2. **Permission Checks**
- Verify user can edit post
- Check nonces on all AJAX requests
- Validate post ownership
3. **Temp Directory**
- Add .htaccess to prevent direct access
- Regular cleanup of old files
- Limit directory size
4. **API Security**
- Never expose API keys to frontend
- Rate limiting on image generation
- Cost limits per user/post
---
## Rollout Strategy
### Phase 1: Beta (Week 1)
- Enable for admin users only
- Test with Budget preset
- Monitor costs and errors
### Phase 2: Limited Release (Week 2)
- Enable for all users
- Add usage limits (e.g., 10 images/day)
- Collect feedback
### Phase 3: Full Release (Week 3)
- Remove limits
- Add admin page for image management
- Documentation and tutorials
---
## Testing Checklist
- [ ] Database tables created successfully
- [ ] Temp directory created with proper permissions
- [ ] Article analysis generates placement points
- [ ] Prompt generation creates model-specific prompts
- [ ] Image generation via OpenRouter works
- [ ] Variants saved to temp directory
- [ ] Variant selection updates Gutenberg block
- [ ] Media Library upload works with alt text
- [ ] Cost tracking records image generation
- [ ] Cleanup cron job removes old temps
- [ ] Error handling for API failures
- [ ] Permission checks work correctly
- [ ] UI responsive on mobile
- [ ] Works with all 3 presets
---
## Success Metrics
- **Cost Efficiency:** Average cost per article with images < $0.15
- **User Adoption:** >50% of users generate at least 1 image
- **Quality:** <10% regeneration rate (first variant selected)
- **Performance:** Image generation completes in <30 seconds
- **Errors:** <5% API failure rate
---
## Next Steps
1. **Review this plan** with team
2. **Create Phase 1 branch** in git
3. **Implement database schema** (2 hours)
4. **Build backend API** (4 hours)
5. **Create frontend modal** (4 hours)
6. **Test end-to-end** (2 hours)
7. **Deploy to staging** for beta testing
---
**End of Implementation Plan**