- 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
1346 lines
46 KiB
Markdown
1346 lines
46 KiB
Markdown
# WP Agentic Writer: Image Generation & Selection Flow
|
|
|
|
## Executive Summary
|
|
|
|
This document defines the **complete lifecycle** of images from agent recommendation → generation → variant management → final WordPress Media upload.
|
|
|
|
**Key principle:** Regenerate creates NEW variants (doesn't delete old ones). All temp images belong to a post. Users see variants in modal, select one, and commit to WordPress Media with recommended alt text.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview & Architecture](#overview--architecture)
|
|
2. [Data Model](#data-model)
|
|
3. [Flow 1: Article Generation & Image Recommendations](#flow-1-article-generation--image-recommendations)
|
|
4. [Flow 2: Image Block Toolbar & Modal](#flow-2-image-block-toolbar--modal)
|
|
5. [Flow 3: Image Generation (Variants)](#flow-3-image-generation-variants)
|
|
6. [Flow 4: Variant Selection & Media Upload](#flow-4-variant-selection--media-upload)
|
|
7. [Flow 5: Temp Image Management](#flow-5-temp-image-management)
|
|
8. [Admin Page: Image Library](#admin-page-image-library)
|
|
9. [REST API Endpoints](#rest-api-endpoints)
|
|
10. [Implementation Checklist](#implementation-checklist)
|
|
|
|
---
|
|
|
|
## Overview & Architecture
|
|
|
|
### Core concept
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ WRITING AGENT generates article + 3 image recommendations │
|
|
└────────────────────┬────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Plugin converts recommendations → 3 core/image blocks │
|
|
│ Each block has: data-agent-image-id="img_X" │
|
|
│ Stores recommendations in wp_agentic_images table │
|
|
└────────────────────┬────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ USER EDITS IN GUTENBERG EDITOR │
|
|
│ │
|
|
│ [Image block] [Image block] [Image block] │
|
|
│ ↓ Generate ↓ Generate ↓ Generate │
|
|
│ ↓ (Toolbar btn) ↓ (Toolbar btn) ↓ (Toolbar btn) │
|
|
│ │
|
|
│ Each opens YOUR modal with prompt + alt editable │
|
|
└────────────────────┬────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ USER ACTIONS IN MODAL │
|
|
│ │
|
|
│ [View Prompt] [Edit Prompt] │
|
|
│ [View Alt] [Edit Alt] │
|
|
│ [Generate] → generates 1-3 variants │
|
|
│ stores ALL in /wp-content/agentic-writer-temp/ │
|
|
│ │
|
|
│ [Regenerate] → generates MORE variants (doesn't delete old) │
|
|
│ adds to same image_id pool │
|
|
│ │
|
|
│ [Use Media Library] → opens core media modal │
|
|
│ with pre-filled alt suggestion │
|
|
└────────────────────┬────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ USER SELECTS A VARIANT │
|
|
│ │
|
|
│ [Variant 1] [Variant 2] [Variant 3] [Variant 4] │
|
|
│ [Select] [Select] [Select] [Select] │
|
|
│ │
|
|
│ Other variants stay in temp folder + DB │
|
|
└────────────────────┬────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ BACKEND: Commit Selected Variant to WP Media │
|
|
│ │
|
|
│ 1. media_handle_sideload(temp_image_path) │
|
|
│ 2. Set attachment alt → recommended alt (or user-edited) │
|
|
│ 3. Update wp_agentic_images: status='committed' │
|
|
│ attachment_id=123 │
|
|
│ 4. Return attachment ID + URL │
|
|
└────────────────────┬────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ FRONTEND: Update Gutenberg Image Block │
|
|
│ │
|
|
│ updateBlockAttributes(block.clientId, { │
|
|
│ id: attachment_id, │
|
|
│ url: attachment_url, │
|
|
│ alt: recommended_alt │
|
|
│ }) │
|
|
│ │
|
|
│ Remove data-agent-image-id (no longer a placeholder) │
|
|
└────────────────────────────────────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ TEMP IMAGE CLEANUP (Manual + Auto) │
|
|
│ │
|
|
│ Admin page "Generated Images" tab shows: │
|
|
│ - All temp images (by post, by status) │
|
|
│ - [Delete selected] [Auto-cleanup old (>7 days)] │
|
|
│ │
|
|
│ Cron job: wp_schedule_event() → delete temps > 7 days │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Data Model
|
|
|
|
### Table: `wp_agentic_images`
|
|
|
|
Stores recommendations + generation history for each image per post.
|
|
|
|
```sql
|
|
CREATE TABLE wp_agentic_images (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
post_id BIGINT NOT NULL,
|
|
|
|
-- Recommendation from agent
|
|
agent_image_id VARCHAR(50) NOT NULL, -- e.g., "img_hero_1"
|
|
placement VARCHAR(100), -- "intro_hero", "after_section_2"
|
|
section_title VARCHAR(255), -- "Introduction to n8n"
|
|
|
|
-- Original recommendation
|
|
prompt_initial TEXT NOT NULL, -- Agent's initial prompt
|
|
alt_text_initial TEXT, -- Agent's suggested alt
|
|
|
|
-- User edits (nullable)
|
|
prompt_edited TEXT, -- Null if user didn't edit
|
|
alt_text_edited TEXT, -- Null if user didn't edit
|
|
|
|
-- Committed image (when user selects a variant)
|
|
attachment_id BIGINT, -- WP attachment ID (null until committed)
|
|
status VARCHAR(30) DEFAULT 'pending', -- pending, generating, committed, discarded
|
|
|
|
-- Cost tracking
|
|
cost_estimate DECIMAL(10, 4), -- Based on image model pricing
|
|
cost_actual DECIMAL(10, 4), -- Updated after generation
|
|
image_model VARCHAR(100), -- Which model was used
|
|
|
|
-- Metadata
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
KEY idx_post (post_id),
|
|
KEY idx_agent_image_id (post_id, agent_image_id),
|
|
KEY idx_status (status),
|
|
KEY idx_created (created_at)
|
|
);
|
|
```
|
|
|
|
### Table: `wp_agentic_images_variants`
|
|
|
|
Tracks all generated variants (temp images) for each agent_image_id.
|
|
|
|
```sql
|
|
CREATE TABLE wp_agentic_images_variants (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
|
-- Reference to main image record
|
|
agentic_image_id BIGINT NOT NULL,
|
|
post_id BIGINT NOT NULL,
|
|
agent_image_id VARCHAR(50) NOT NULL, -- e.g., "img_hero_1"
|
|
|
|
-- Variant details
|
|
variant_number INT DEFAULT 1, -- 1st, 2nd, 3rd generation attempt
|
|
temp_file_path VARCHAR(500) NOT NULL, -- /wp-content/agentic-writer-temp/xxx.jpg
|
|
temp_file_url VARCHAR(500) NOT NULL, -- URL to temp image
|
|
file_size INT, -- In bytes
|
|
|
|
-- Generation details
|
|
prompt_used TEXT, -- Exact prompt sent to image model
|
|
image_model_used VARCHAR(100), -- Which model generated this
|
|
generation_time INT, -- Seconds to generate
|
|
cost DECIMAL(10, 4), -- Cost of this generation
|
|
|
|
-- Selection status
|
|
is_selected TINYINT DEFAULT 0, -- 1 if user selected this variant
|
|
selected_at TIMESTAMP NULL,
|
|
|
|
-- Lifecycle
|
|
status VARCHAR(30) DEFAULT 'temp', -- temp, selected, discarded, auto_deleted
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at TIMESTAMP NULL,
|
|
|
|
KEY idx_agentic_image (agentic_image_id),
|
|
KEY idx_post (post_id),
|
|
KEY idx_status (status),
|
|
KEY idx_created (created_at)
|
|
);
|
|
```
|
|
|
|
### File system: Temp images
|
|
|
|
```
|
|
/wp-content/agentic-writer-temp/
|
|
├── post_<POST_ID>/
|
|
│ ├── img_hero_1/
|
|
│ │ ├── variant_1.jpg
|
|
│ │ ├── variant_2.jpg
|
|
│ │ └── variant_3.jpg
|
|
│ ├── img_diag_1/
|
|
│ │ └── variant_1.jpg
|
|
│ └── img_section_2/
|
|
│ ├── variant_1.jpg
|
|
│ └── variant_2.jpg
|
|
└── [cleanup cron removes files 7+ days old]
|
|
```
|
|
|
|
---
|
|
|
|
## Flow 1: Article Generation & Image Recommendations
|
|
|
|
### What the agent returns
|
|
|
|
After writing article, agent provides JSON response:
|
|
|
|
```json
|
|
{
|
|
"status": "article_complete",
|
|
"article_blocks": [
|
|
{
|
|
"blockName": "core/paragraph",
|
|
"attrs": {},
|
|
"innerBlocks": [],
|
|
"innerHTML": "<p>Introduction text...</p>"
|
|
},
|
|
{
|
|
"blockName": "core/image",
|
|
"attrs": {
|
|
"id": null,
|
|
"url": null,
|
|
"alt": "",
|
|
"data-agent-image-id": "img_hero_1"
|
|
}
|
|
}
|
|
],
|
|
"images": [
|
|
{
|
|
"agent_image_id": "img_hero_1",
|
|
"placement": "intro_hero",
|
|
"section_title": "Introduction",
|
|
"prompt": "N8n workflow automation dashboard...",
|
|
"alt": "N8n automation dashboard with workflow nodes",
|
|
"image_model": "sourceful/riverflow-v2-max"
|
|
},
|
|
{
|
|
"agent_image_id": "img_diag_1",
|
|
"placement": "after_section_2",
|
|
"section_title": "How Workflows Run",
|
|
"prompt": "Workflow architecture diagram...",
|
|
"alt": "Workflow trigger-condition-action diagram",
|
|
"image_model": "sourceful/riverflow-v2-max"
|
|
},
|
|
{
|
|
"agent_image_id": "img_section_4",
|
|
"placement": "before_conclusion",
|
|
"section_title": "Real-world Example",
|
|
"prompt": "Developer using N8n dashboard...",
|
|
"alt": "Developer working with N8n automation dashboard",
|
|
"image_model": "sourceful/riverflow-v2-max"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Backend handling
|
|
|
|
```php
|
|
<?php
|
|
/**
|
|
* On article completion, save image recommendations
|
|
*/
|
|
public static function save_article_with_images( $post_id, $agent_response ) {
|
|
// 1. Insert article blocks
|
|
$post_content = wp_json_encode( $agent_response['article_blocks'] );
|
|
wp_update_post([
|
|
'ID' => $post_id,
|
|
'post_content' => $post_content,
|
|
'post_status' => 'draft'
|
|
]);
|
|
|
|
// 2. Save each image recommendation
|
|
foreach ( $agent_response['images'] as $image_spec ) {
|
|
self::save_image_recommendation(
|
|
$post_id,
|
|
$image_spec
|
|
);
|
|
}
|
|
|
|
// 3. Return success message for chat
|
|
return [
|
|
'status' => 'article_complete',
|
|
'post_id' => $post_id,
|
|
'message' => sprintf(
|
|
'Article created. %d image suggestions ready in the Images panel.',
|
|
count( $agent_response['images'] )
|
|
)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Store individual image recommendation
|
|
*/
|
|
private static function save_image_recommendation( $post_id, $image_spec ) {
|
|
global $wpdb;
|
|
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'agentic_images',
|
|
[
|
|
'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'
|
|
],
|
|
[ '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ]
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Flow 2: Image Block Toolbar & Modal
|
|
|
|
### Block toolbar button
|
|
|
|
In Gutenberg, each `core/image` block with `data-agent-image-id` gets a toolbar button:
|
|
|
|
```jsx
|
|
// registerPlugin('agentic-image-toolbar', {
|
|
// render() {
|
|
// return <ImageBlockToolbar />
|
|
// }
|
|
// })
|
|
|
|
function ImageBlockToolbar() {
|
|
const { selectedBlockClientId } = useSelect(blockEditorStore);
|
|
const block = useSelect(
|
|
select => select(blockEditorStore).getBlock(selectedBlockClientId),
|
|
[selectedBlockClientId]
|
|
);
|
|
|
|
if (!block || block.name !== 'core/image') return null;
|
|
|
|
const agentImageId = block.attributes['data-agent-image-id'];
|
|
if (!agentImageId) return null; // Not an agent placeholder
|
|
|
|
return (
|
|
<BlockControls>
|
|
<ToolbarButton
|
|
label="Generate Image"
|
|
onClick={() => openImageModal(agentImageId, block)}
|
|
icon="image"
|
|
/>
|
|
</BlockControls>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Opens YOUR custom modal (not WP media modal)
|
|
*/
|
|
function openImageModal(agentImageId, block) {
|
|
wp.data.dispatch('agentic-writer').openImageGenerationModal({
|
|
agentImageId,
|
|
blockClientId: block.clientId,
|
|
postId: wp.data.select('core/editor').getCurrentPostId()
|
|
});
|
|
}
|
|
```
|
|
|
|
### Your custom modal
|
|
|
|
```jsx
|
|
function ImageGenerationModal({ agentImageId, blockClientId, postId }) {
|
|
const [prompt, setPrompt] = useState(initialPrompt);
|
|
const [alt, setAlt] = useState(initialAlt);
|
|
const [variants, setVariants] = useState([]);
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
const [step, setStep] = useState('edit'); // 'edit' | 'generating' | 'select'
|
|
|
|
const handleGenerate = async () => {
|
|
setIsGenerating(true);
|
|
setStep('generating');
|
|
|
|
try {
|
|
const response = await fetch('/wp-json/agentic-writer/v1/generate-image', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
post_id: postId,
|
|
agent_image_id: agentImageId,
|
|
prompt: prompt, // User-edited if changed
|
|
alt: alt // User-edited if changed
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
setVariants(result.variants);
|
|
setStep('select');
|
|
} catch (error) {
|
|
console.error('Generation failed:', error);
|
|
} finally {
|
|
setIsGenerating(false);
|
|
}
|
|
};
|
|
|
|
const handleRegenerate = async () => {
|
|
// Re-generate: creates MORE variants
|
|
// Does NOT delete existing ones
|
|
await handleGenerate();
|
|
};
|
|
|
|
const handleSelect = async (variantId) => {
|
|
// Commit this variant to WP Media
|
|
const response = await fetch('/wp-json/agentic-writer/v1/commit-image', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
post_id: postId,
|
|
agent_image_id: agentImageId,
|
|
variant_id: variantId,
|
|
alt: alt // Final alt text
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
// Update Gutenberg block
|
|
wp.data.dispatch('core/block-editor').updateBlockAttributes(
|
|
blockClientId,
|
|
{
|
|
id: result.attachment_id,
|
|
url: result.attachment_url,
|
|
alt: result.alt,
|
|
'data-agent-image-id': undefined // Remove placeholder marker
|
|
}
|
|
);
|
|
|
|
// Close modal
|
|
wp.data.dispatch('agentic-writer').closeImageGenerationModal();
|
|
};
|
|
|
|
if (step === 'edit') {
|
|
return (
|
|
<Modal title="Generate Image" onRequestClose={closeModal}>
|
|
<div className="agentic-image-modal">
|
|
<div className="section">
|
|
<label>Prompt:</label>
|
|
<textarea
|
|
value={prompt}
|
|
onChange={e => setPrompt(e.target.value)}
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
|
|
<div className="section">
|
|
<label>Alt Text:</label>
|
|
<input
|
|
type="text"
|
|
value={alt}
|
|
onChange={e => setAlt(e.target.value)}
|
|
placeholder="Alt text for accessibility"
|
|
/>
|
|
</div>
|
|
|
|
<div className="actions">
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleGenerate}
|
|
disabled={isGenerating}
|
|
>
|
|
{isGenerating ? 'Generating...' : 'Generate'}
|
|
</Button>
|
|
<Button variant="secondary" onClick={closeModal}>
|
|
Use Media Library
|
|
</Button>
|
|
<Button variant="tertiary" onClick={closeModal}>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
if (step === 'generating') {
|
|
return (
|
|
<Modal title="Generating..." onRequestClose={() => {}}>
|
|
<div className="generating-spinner">
|
|
<Spinner /> Generating image variants...
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
// step === 'select'
|
|
return (
|
|
<Modal title="Select Variant" onRequestClose={closeModal}>
|
|
<div className="variant-grid">
|
|
{variants.map(variant => (
|
|
<div key={variant.id} className="variant-card">
|
|
<img src={variant.temp_file_url} alt="variant" />
|
|
<div className="variant-info">
|
|
<p>Generation: {variant.variant_number}</p>
|
|
<p>Cost: ${variant.cost.toFixed(3)}</p>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => handleSelect(variant.id)}
|
|
>
|
|
Select
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="modal-footer">
|
|
<Button variant="secondary" onClick={handleRegenerate}>
|
|
Regenerate (More Variants)
|
|
</Button>
|
|
<Button variant="tertiary" onClick={closeModal}>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Flow 3: Image Generation (Variants)
|
|
|
|
### Backend: Generate endpoint
|
|
|
|
```php
|
|
<?php
|
|
/**
|
|
* POST /wp-json/agentic-writer/v1/generate-image
|
|
*
|
|
* Generates 1-3 variants of an image based on prompt.
|
|
* Does NOT delete previous variants.
|
|
*/
|
|
public static function rest_generate_image( WP_REST_Request $request ) {
|
|
$post_id = $request->get_param('post_id');
|
|
$agent_image_id = $request->get_param('agent_image_id');
|
|
$prompt = $request->get_param('prompt');
|
|
$alt = $request->get_param('alt');
|
|
|
|
// Validate post ownership
|
|
if (!current_user_can('edit_post', $post_id)) {
|
|
return new WP_Error('unauthorized', 'Not allowed', ['status' => 403]);
|
|
}
|
|
|
|
// Get image record
|
|
$image_record = self::get_image_record($post_id, $agent_image_id);
|
|
if (!$image_record) {
|
|
return new WP_Error('not_found', 'Image not found', ['status' => 404]);
|
|
}
|
|
|
|
// Get config (model, preset, etc)
|
|
$image_model = $image_record->image_model;
|
|
$num_variants = apply_filters('agentic_writer_num_variants', 3); // Default: 3
|
|
|
|
// Create temp directory for this image
|
|
$temp_dir = self::get_temp_dir_for_image($post_id, $agent_image_id);
|
|
if (!is_dir($temp_dir)) {
|
|
wp_mkdir_p($temp_dir);
|
|
}
|
|
|
|
// Update status
|
|
self::update_image_record($image_record->id, ['status' => 'generating']);
|
|
|
|
$variants = [];
|
|
$total_cost = 0;
|
|
|
|
// Generate N variants (sequentially or in batch)
|
|
for ($i = 1; $i <= $num_variants; $i++) {
|
|
$variant_result = self::generate_single_variant(
|
|
$image_model,
|
|
$prompt,
|
|
$temp_dir,
|
|
$i,
|
|
$post_id,
|
|
$agent_image_id
|
|
);
|
|
|
|
if (is_wp_error($variant_result)) {
|
|
// Log error but continue (partial success ok)
|
|
error_log("Variant $i failed: " . $variant_result->get_error_message());
|
|
continue;
|
|
}
|
|
|
|
$variants[] = $variant_result;
|
|
$total_cost += $variant_result['cost'];
|
|
}
|
|
|
|
// Update main image record
|
|
self::update_image_record($image_record->id, [
|
|
'prompt_edited' => $prompt, // User's edited prompt (if changed from initial)
|
|
'alt_text_edited' => $alt, // User's edited alt (if changed from initial)
|
|
'cost_actual' => $total_cost,
|
|
'status' => 'pending' // Waiting for user to select variant
|
|
]);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'variants' => $variants,
|
|
'total_cost' => $total_cost,
|
|
'message' => sprintf(
|
|
'Generated %d variants. Total cost: $%.3f',
|
|
count($variants),
|
|
$total_cost
|
|
)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Generate a single variant and save to temp folder
|
|
*/
|
|
private static function generate_single_variant(
|
|
$image_model,
|
|
$prompt,
|
|
$temp_dir,
|
|
$variant_number,
|
|
$post_id,
|
|
$agent_image_id
|
|
) {
|
|
// Get generation time start
|
|
$start_time = microtime(true);
|
|
|
|
// Call image generation API
|
|
$api_response = self::call_image_api(
|
|
$image_model,
|
|
$prompt
|
|
);
|
|
|
|
if (is_wp_error($api_response)) {
|
|
return $api_response;
|
|
}
|
|
|
|
$generation_time = microtime(true) - $start_time;
|
|
|
|
// Download image and save to temp folder
|
|
$temp_filename = sprintf(
|
|
'variant_%d_%d.jpg',
|
|
$variant_number,
|
|
time()
|
|
);
|
|
$temp_filepath = $temp_dir . '/' . $temp_filename;
|
|
|
|
$download_result = self::download_image(
|
|
$api_response['url'],
|
|
$temp_filepath
|
|
);
|
|
|
|
if (is_wp_error($download_result)) {
|
|
return $download_result;
|
|
}
|
|
|
|
$file_size = filesize($temp_filepath);
|
|
$cost = self::calculate_image_cost($image_model, $file_size);
|
|
$temp_file_url = str_replace(
|
|
ABSPATH,
|
|
site_url() . '/',
|
|
$temp_filepath
|
|
);
|
|
|
|
// Save variant record to DB
|
|
global $wpdb;
|
|
$variant_id = $wpdb->insert_id = $wpdb->insert(
|
|
$wpdb->prefix . 'agentic_images_variants',
|
|
[
|
|
'agentic_image_id' => $image_record->id,
|
|
'post_id' => $post_id,
|
|
'agent_image_id' => $agent_image_id,
|
|
'variant_number' => $variant_number,
|
|
'temp_file_path' => $temp_filepath,
|
|
'temp_file_url' => $temp_file_url,
|
|
'file_size' => $file_size,
|
|
'prompt_used' => $prompt,
|
|
'image_model_used' => $image_model,
|
|
'generation_time' => $generation_time,
|
|
'cost' => $cost,
|
|
'status' => 'temp'
|
|
]
|
|
);
|
|
|
|
return [
|
|
'id' => $variant_id,
|
|
'temp_file_url' => $temp_file_url,
|
|
'variant_number' => $variant_number,
|
|
'cost' => $cost,
|
|
'generation_time' => $generation_time
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Call OpenRouter image generation API
|
|
*/
|
|
private static function call_image_api($image_model, $prompt) {
|
|
$api_key = get_option('agentic_writer_openrouter_api_key');
|
|
|
|
$response = wp_remote_post(
|
|
'https://openrouter.ai/api/v1/images/generations',
|
|
[
|
|
'headers' => [
|
|
'Authorization' => 'Bearer ' . $api_key,
|
|
'Content-Type' => 'application/json',
|
|
],
|
|
'body' => wp_json_encode([
|
|
'model' => $image_model,
|
|
'prompt' => $prompt,
|
|
'n' => 1,
|
|
'size' => '1024x576', // Standard blog size
|
|
'quality' => 'hd'
|
|
]),
|
|
'timeout' => 60
|
|
]
|
|
);
|
|
|
|
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',
|
|
'Image API error: ' . ($body['error']['message'] ?? 'Unknown error')
|
|
);
|
|
}
|
|
|
|
return [
|
|
'url' => $body['data'][0]['url'],
|
|
'cost' => $body['usage']['cost'] ?? 0.03 // Fallback estimate
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Download image from URL to temp folder
|
|
*/
|
|
private static function download_image($url, $filepath) {
|
|
$response = wp_remote_get($url);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
$body = wp_remote_retrieve_body($response);
|
|
|
|
if (file_put_contents($filepath, $body) === false) {
|
|
return new WP_Error('file_write_failed', 'Could not write temp image file');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* IMPORTANT: Regenerate does NOT delete old variants
|
|
* It just adds new ones
|
|
*/
|
|
private static function get_temp_dir_for_image($post_id, $agent_image_id) {
|
|
return WP_CONTENT_DIR . '/agentic-writer-temp/post_' . $post_id . '/' . $agent_image_id;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Flow 4: Variant Selection & Media Upload
|
|
|
|
### Backend: Commit endpoint
|
|
|
|
```php
|
|
<?php
|
|
/**
|
|
* POST /wp-json/agentic-writer/v1/commit-image
|
|
*
|
|
* User selected a variant. Upload to WordPress Media.
|
|
*/
|
|
public static function rest_commit_image( WP_REST_Request $request ) {
|
|
$post_id = $request->get_param('post_id');
|
|
$agent_image_id = $request->get_param('agent_image_id');
|
|
$variant_id = $request->get_param('variant_id');
|
|
$alt = $request->get_param('alt');
|
|
|
|
// Validate
|
|
if (!current_user_can('edit_post', $post_id)) {
|
|
return new WP_Error('unauthorized', 'Not allowed', ['status' => 403]);
|
|
}
|
|
|
|
// Get variant
|
|
$variant = self::get_variant($variant_id);
|
|
if (!$variant || $variant->post_id != $post_id) {
|
|
return new WP_Error('not_found', 'Variant not found', ['status' => 404]);
|
|
}
|
|
|
|
// Check temp file exists
|
|
if (!file_exists($variant->temp_file_path)) {
|
|
return new WP_Error('file_not_found', 'Temp image not found');
|
|
}
|
|
|
|
// Upload to WordPress Media using sideload
|
|
$attachment_id = self::sideload_image_to_media(
|
|
$variant->temp_file_path,
|
|
$post_id,
|
|
$alt
|
|
);
|
|
|
|
if (is_wp_error($attachment_id)) {
|
|
return $attachment_id;
|
|
}
|
|
|
|
// Get attachment URL
|
|
$attachment_url = wp_get_attachment_url($attachment_id);
|
|
|
|
// Update main image record
|
|
self::update_image_record_by_agent_image_id($post_id, $agent_image_id, [
|
|
'attachment_id' => $attachment_id,
|
|
'status' => 'committed'
|
|
]);
|
|
|
|
// Mark this variant as selected
|
|
self::update_variant($variant_id, [
|
|
'is_selected' => 1,
|
|
'selected_at' => current_time('mysql'),
|
|
'status' => 'selected'
|
|
]);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'attachment_id' => $attachment_id,
|
|
'attachment_url' => $attachment_url,
|
|
'alt' => $alt,
|
|
'message' => 'Image committed to WordPress Media.'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sideload temp image to WordPress Media
|
|
*/
|
|
private static function sideload_image_to_media(
|
|
$temp_filepath,
|
|
$post_id,
|
|
$alt_text
|
|
) {
|
|
// Use WordPress sideload
|
|
require_once(ABSPATH . 'wp-admin/includes/image.php');
|
|
require_once(ABSPATH . 'wp-admin/includes/file.php');
|
|
require_once(ABSPATH . 'wp-admin/includes/media.php');
|
|
|
|
$filename = basename($temp_filepath);
|
|
$file_array = [
|
|
'name' => $filename,
|
|
'tmp_name' => $temp_filepath
|
|
];
|
|
|
|
$attachment_id = media_handle_sideload(
|
|
$file_array,
|
|
$post_id
|
|
);
|
|
|
|
if (is_wp_error($attachment_id)) {
|
|
return $attachment_id;
|
|
}
|
|
|
|
// Set alt text
|
|
update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
|
|
|
|
return $attachment_id;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Flow 5: Temp Image Management
|
|
|
|
### Temp image lifecycle
|
|
|
|
```
|
|
1. Generated → stored in /agentic-writer-temp/post_X/img_Y/variant_N.jpg
|
|
Status: 'temp'
|
|
|
|
2a. User selects variant → moved to WP Media Library
|
|
Status: 'selected' (in wp_agentic_images_variants)
|
|
Status: 'committed' (in wp_agentic_images)
|
|
|
|
2b. User regenerates (doesn't delete old variants)
|
|
→ New variants created in SAME folder
|
|
→ Old variants status remains 'temp'
|
|
|
|
3. Non-selected temps persist in DB until:
|
|
- User manually deletes from admin page
|
|
- Cron job runs (auto-cleanup > 7 days)
|
|
|
|
Status transitions: 'temp' → 'discarded' → deleted from disk
|
|
```
|
|
|
|
### Backend: List temp images endpoint
|
|
|
|
```php
|
|
<?php
|
|
/**
|
|
* GET /wp-json/agentic-writer/v1/temp-images?post_id=123
|
|
*
|
|
* Show all temp images for a post
|
|
*/
|
|
public static function rest_list_temp_images( WP_REST_Request $request ) {
|
|
$post_id = $request->get_param('post_id');
|
|
|
|
if (!current_user_can('edit_post', $post_id)) {
|
|
return new WP_Error('unauthorized', 'Not allowed', ['status' => 403]);
|
|
}
|
|
|
|
global $wpdb;
|
|
|
|
$temps = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT v.*, i.agent_image_id, i.prompt_initial, i.alt_text_initial
|
|
FROM {$wpdb->prefix}agentic_images_variants v
|
|
JOIN {$wpdb->prefix}agentic_images i ON v.agentic_image_id = i.id
|
|
WHERE v.post_id = %d
|
|
AND v.status IN ('temp', 'discarded')
|
|
ORDER BY v.created_at DESC",
|
|
$post_id
|
|
));
|
|
|
|
return new WP_REST_Response([
|
|
'total' => count($temps),
|
|
'images' => $temps
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* DELETE /wp-json/agentic-writer/v1/temp-images/<variant_id>
|
|
*
|
|
* Manually delete a temp image
|
|
*/
|
|
public static function rest_delete_temp_image( WP_REST_Request $request ) {
|
|
$variant_id = $request->get_param('variant_id');
|
|
|
|
$variant = self::get_variant($variant_id);
|
|
if (!$variant) {
|
|
return new WP_Error('not_found', 'Variant not found');
|
|
}
|
|
|
|
if (!current_user_can('edit_post', $variant->post_id)) {
|
|
return new WP_Error('unauthorized', 'Not allowed');
|
|
}
|
|
|
|
// Delete file
|
|
if (file_exists($variant->temp_file_path)) {
|
|
unlink($variant->temp_file_path);
|
|
}
|
|
|
|
// Mark as deleted in DB
|
|
self::update_variant($variant_id, [
|
|
'status' => 'discarded',
|
|
'deleted_at' => current_time('mysql')
|
|
]);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'message' => 'Temp image deleted.'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Cron job: Auto-cleanup old temps (> 7 days)
|
|
*/
|
|
public static function cleanup_old_temp_images() {
|
|
global $wpdb;
|
|
|
|
$cutoff_date = date('Y-m-d H:i:s', strtotime('-7 days'));
|
|
|
|
// Get old temp images
|
|
$old_temps = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}agentic_images_variants
|
|
WHERE status = 'temp'
|
|
AND created_at < %s",
|
|
$cutoff_date
|
|
));
|
|
|
|
foreach ($old_temps as $temp) {
|
|
// Delete file
|
|
if (file_exists($temp->temp_file_path)) {
|
|
unlink($temp->temp_file_path);
|
|
}
|
|
|
|
// Mark as auto-deleted
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'agentic_images_variants',
|
|
[
|
|
'status' => 'auto_deleted',
|
|
'deleted_at' => current_time('mysql')
|
|
],
|
|
[ 'id' => $temp->id ]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Hook into WordPress cron
|
|
add_action('agentic_writer_cleanup_temps', [__CLASS__, 'cleanup_old_temp_images']);
|
|
|
|
// Schedule once
|
|
if (!wp_next_scheduled('agentic_writer_cleanup_temps')) {
|
|
wp_schedule_event(
|
|
time(),
|
|
'daily',
|
|
'agentic_writer_cleanup_temps'
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Admin Page: Image Library
|
|
|
|
### New tab in plugin settings
|
|
|
|
```
|
|
Plugin Settings → Generated Images (new tab)
|
|
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ Generated Images Library │
|
|
├────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Filter by: [Post ▼] [Status ▼] [Date ▼] │
|
|
│ │
|
|
│ [Select All] [Delete Selected] [Auto-cleanup Old] │
|
|
│ │
|
|
│ ───────────────────────────────────────────────────── │
|
|
│ │
|
|
│ [Thumb] Post: "Article Title" │
|
|
│ Agent Image ID: img_hero_1 │
|
|
│ Status: temp │
|
|
│ Created: Jan 27, 2026 2:30 PM │
|
|
│ Cost: $0.03 │
|
|
│ [ ] [View] [Delete] │
|
|
│ │
|
|
│ [Thumb] Post: "Article Title" │
|
|
│ Agent Image ID: img_diag_1 │
|
|
│ Status: selected (used in Media Library) │
|
|
│ Created: Jan 27, 2026 2:25 PM │
|
|
│ Cost: $0.03 │
|
|
│ [ ] [View] │
|
|
│ │
|
|
│ ───────────────────────────────────────────────────── │
|
|
│ │
|
|
│ Total cost (all time): $123.45 │
|
|
│ Total temps (>7 days): 12 images (ready for cleanup) │
|
|
│ │
|
|
│ [Auto-cleanup Now] │
|
|
│ │
|
|
└────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Implementation
|
|
|
|
```php
|
|
<?php
|
|
/**
|
|
* Admin page: Generated Images library
|
|
*/
|
|
public static function render_admin_generated_images() {
|
|
global $wpdb;
|
|
|
|
$post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : null;
|
|
$status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : null;
|
|
|
|
$query = "SELECT v.*, i.agent_image_id, i.section_title, p.post_title
|
|
FROM {$wpdb->prefix}agentic_images_variants v
|
|
JOIN {$wpdb->prefix}agentic_images i ON v.agentic_image_id = i.id
|
|
JOIN {$wpdb->posts} p ON v.post_id = p.ID
|
|
WHERE 1=1";
|
|
|
|
if ($post_id) {
|
|
$query .= $wpdb->prepare(" AND v.post_id = %d", $post_id);
|
|
}
|
|
|
|
if ($status) {
|
|
$query .= $wpdb->prepare(" AND v.status = %s", $status);
|
|
}
|
|
|
|
$query .= " ORDER BY v.created_at DESC";
|
|
|
|
$images = $wpdb->get_results($query);
|
|
|
|
$total_cost = $wpdb->get_var(
|
|
"SELECT SUM(cost) FROM {$wpdb->prefix}agentic_images_variants"
|
|
);
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Generated Images Library</h1>
|
|
|
|
<div class="filters">
|
|
<input type="hidden" name="page" value="agentic-writer-settings">
|
|
<input type="hidden" name="tab" value="generated_images">
|
|
|
|
<select name="post_id">
|
|
<option value="">All Posts</option>
|
|
<?php foreach (get_posts(['numberposts' => -1]) as $post): ?>
|
|
<option value="<?php echo $post->ID; ?>"<?php selected($post_id, $post->ID); ?>>
|
|
<?php echo esc_html($post->post_title); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
|
|
<select name="status">
|
|
<option value="">All Statuses</option>
|
|
<option value="temp"<?php selected($status, 'temp'); ?>>Temp</option>
|
|
<option value="selected"<?php selected($status, 'selected'); ?>>Selected</option>
|
|
<option value="discarded"<?php selected($status, 'discarded'); ?>>Discarded</option>
|
|
</select>
|
|
|
|
<button type="submit" class="button">Filter</button>
|
|
</div>
|
|
|
|
<form method="post">
|
|
<?php wp_nonce_field('agentic_images_bulk_action'); ?>
|
|
|
|
<table class="wp-list-table widefat">
|
|
<thead>
|
|
<tr>
|
|
<td class="check-column"><input type="checkbox" /></td>
|
|
<th>Image</th>
|
|
<th>Post</th>
|
|
<th>Agent Image ID</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th>Cost</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($images as $image): ?>
|
|
<tr>
|
|
<td><input type="checkbox" name="variants[]" value="<?php echo $image->id; ?>" /></td>
|
|
<td>
|
|
<a href="<?php echo $image->temp_file_url; ?>" target="_blank">
|
|
<img src="<?php echo $image->temp_file_url; ?>"
|
|
style="max-width: 100px; max-height: 100px;">
|
|
</a>
|
|
</td>
|
|
<td><?php echo esc_html($image->post_title); ?></td>
|
|
<td><?php echo esc_html($image->agent_image_id); ?></td>
|
|
<td><span class="status-badge status-<?php echo $image->status; ?>">
|
|
<?php echo ucfirst($image->status); ?>
|
|
</span></td>
|
|
<td><?php echo date_i18n('M d, Y g:i A', strtotime($image->created_at)); ?></td>
|
|
<td>$<?php echo number_format($image->cost, 3); ?></td>
|
|
<td>
|
|
<a href="<?php echo $image->temp_file_url; ?>" target="_blank">View</a>
|
|
<?php if ($image->status === 'temp'): ?>
|
|
| <a href="#" class="delete-variant" data-variant-id="<?php echo $image->id; ?>">Delete</a>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="bulk-actions">
|
|
<select name="action">
|
|
<option value="">Bulk Actions</option>
|
|
<option value="delete">Delete Selected</option>
|
|
</select>
|
|
<button type="submit" class="button">Apply</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="summary-box">
|
|
<p><strong>Total Cost (all time):</strong> $<?php echo number_format($total_cost, 2); ?></p>
|
|
<p><strong>Temp Images Ready for Cleanup (>7 days):</strong>
|
|
<?php echo $wpdb->get_var(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}agentic_images_variants
|
|
WHERE status = 'temp' AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)"
|
|
); ?>
|
|
</p>
|
|
<button type="button" class="button button-primary" id="cleanup-old">Auto-cleanup Old Temps</button>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## REST API Endpoints
|
|
|
|
### Summary
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/agentic-writer/v1/generate-image` | POST | Start generation (creates variants) |
|
|
| `/agentic-writer/v1/commit-image` | POST | Select variant + upload to Media |
|
|
| `/agentic-writer/v1/temp-images` | GET | List all temp images for post |
|
|
| `/agentic-writer/v1/temp-images/<id>` | DELETE | Delete a specific temp image |
|
|
|
|
### Endpoint definitions
|
|
|
|
```php
|
|
<?php
|
|
add_action('rest_api_init', function() {
|
|
register_rest_route('agentic-writer/v1', '/generate-image', [
|
|
'methods' => 'POST',
|
|
'callback' => [Agentic_Writer_Images::class, 'rest_generate_image'],
|
|
'permission_callback' => '__return_true' // Checked inside function
|
|
]);
|
|
|
|
register_rest_route('agentic-writer/v1', '/commit-image', [
|
|
'methods' => 'POST',
|
|
'callback' => [Agentic_Writer_Images::class, 'rest_commit_image'],
|
|
'permission_callback' => '__return_true'
|
|
]);
|
|
|
|
register_rest_route('agentic-writer/v1', '/temp-images', [
|
|
'methods' => 'GET',
|
|
'callback' => [Agentic_Writer_Images::class, 'rest_list_temp_images'],
|
|
'permission_callback' => '__return_true'
|
|
]);
|
|
|
|
register_rest_route('agentic-writer/v1', '/temp-images/(?P<variant_id>\d+)', [
|
|
'methods' => 'DELETE',
|
|
'callback' => [Agentic_Writer_Images::class, 'rest_delete_temp_image'],
|
|
'permission_callback' => '__return_true'
|
|
]);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Phase 1: Database & Data Model
|
|
- [ ] Create `wp_agentic_images` table
|
|
- [ ] Create `wp_agentic_images_variants` table
|
|
- [ ] Add migration script
|
|
|
|
### Phase 2: Agent Response Handling
|
|
- [ ] Update agent execution to return `images` array
|
|
- [ ] Save recommendations to `wp_agentic_images`
|
|
- [ ] Insert `core/image` blocks with `data-agent-image-id`
|
|
|
|
### Phase 3: Gutenberg Integration
|
|
- [ ] Register plugin for block toolbar button
|
|
- [ ] Create modal component (Edit → Generate → Select flow)
|
|
- [ ] Implement `updateBlockAttributes` to commit selection
|
|
|
|
### Phase 4: Backend Image Generation
|
|
- [ ] Implement `POST /generate-image` endpoint
|
|
- [ ] Handle variant generation (1-3 images per call)
|
|
- [ ] Save variants to temp folder + DB
|
|
- [ ] **Note:** Regenerate creates NEW variants, doesn't delete old
|
|
|
|
### Phase 5: Media Upload
|
|
- [ ] Implement `POST /commit-image` endpoint
|
|
- [ ] Use `media_handle_sideload()` to upload
|
|
- [ ] Set attachment alt from user-edited value
|
|
- [ ] Update Gutenberg block attributes
|
|
|
|
### Phase 6: Temp Management
|
|
- [ ] Implement `GET /temp-images` endpoint
|
|
- [ ] Implement `DELETE /temp-images/<id>` endpoint
|
|
- [ ] Set up cron job for auto-cleanup (>7 days)
|
|
- [ ] Create cleanup directory function
|
|
|
|
### Phase 7: Admin Page
|
|
- [ ] Add "Generated Images" tab in plugin settings
|
|
- [ ] List all temps/selected images with filters
|
|
- [ ] Bulk delete action
|
|
- [ ] Manual cleanup button
|
|
- [ ] Cost summary display
|
|
|
|
### Phase 8: Testing & UX Polish
|
|
- [ ] Test full generation → selection → upload flow
|
|
- [ ] Test regenerate (old variants persist)
|
|
- [ ] Test cleanup (old temps auto-delete at 7+ days)
|
|
- [ ] Test admin page filtering + deletion
|
|
- [ ] Verify cost tracking accuracy
|
|
|
|
---
|
|
|
|
## Key Notes
|
|
|
|
### Regenerate Behavior
|
|
|
|
```
|
|
User clicks "Regenerate" in modal
|
|
→ Calls POST /generate-image AGAIN with SAME prompt
|
|
→ Backend generates 3 NEW variants
|
|
→ NEW variants saved to DB (variant_number = 2, 3, 4, etc)
|
|
→ OLD variants (variant_number = 1) remain in DB with status='temp'
|
|
→ Temp folder now has: variant_1.jpg, variant_2.jpg, variant_3.jpg, variant_4.jpg, ...
|
|
→ Modal shows ALL variants (user can pick from any)
|
|
→ Selected variant gets attached to post
|
|
→ Non-selected remain as 'temp' until manual delete or cron cleanup
|
|
```
|
|
|
|
### Cost Transparency
|
|
|
|
```
|
|
Each variant_number records:
|
|
- cost: Individual image cost
|
|
- prompt_used: Exact prompt for that generation
|
|
- generation_time: How long it took
|
|
|
|
Main image record:
|
|
- cost_actual: SUM of all variants generated
|
|
- image_model: Which model was used
|
|
- prompt_edited: User's final (possibly edited) prompt
|
|
- alt_text_edited: User's final alt
|
|
```
|
|
|
|
### File Structure (Example)
|
|
|
|
```
|
|
/wp-content/agentic-writer-temp/
|
|
├── post_123/
|
|
│ ├── img_hero_1/
|
|
│ │ ├── variant_1_1706364000.jpg (first generation)
|
|
│ │ ├── variant_2_1706364015.jpg (first generation, variant 2)
|
|
│ │ ├── variant_3_1706364030.jpg (first generation, variant 3)
|
|
│ │ ├── variant_1_1706364060.jpg (second generation, regenerate)
|
|
│ │ ├── variant_2_1706364075.jpg (second generation, regenerate)
|
|
│ │ └── variant_3_1706364090.jpg (second generation, regenerate)
|
|
│ └── img_diag_1/
|
|
│ └── variant_1_1706364120.jpg (only 1 generated)
|
|
└── post_456/
|
|
└── img_hero_1/
|
|
└── variant_1_1706364150.jpg
|
|
```
|
|
|
|
---
|
|
|
|
**Document version:** 1.0
|
|
**Date:** January 27, 2026
|
|
**Status:** Ready for Phase 1 implementation
|