- Implement local backend AI provider with Ollama integration - Add Brave Search API integration for real-time search suggestions - Add image generation manager with multiple AI providers - Create hybrid provider system with local/cloud fallback - Add comprehensive settings UI with provider management - Implement Gutenberg sidebar with writing assistance controls - Add SEO schema generation for AI-generated content - Multiple provider support: OpenRouter, local backend, Codex
1390 lines
53 KiB
Markdown
1390 lines
53 KiB
Markdown
# 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**
|