Major refactoring cleanup: - Add new controller architecture (class-controller-*.php) - Add new settings-v2 UI (views/settings-v2/) - Add new CSS architecture (agentic-sidebar.css, tokens) - Add esbuild build pipeline (scripts/build.js, package.json) - Add composer dependencies (vendor/) - Add frontend src directory (assets/js/src/index.jsx) - Add documentation files - Remove old/obsolete files (class-settings.php, old CSS) This commits all pending changes from previous refactoring efforts.
928 lines
28 KiB
PHP
928 lines
28 KiB
PHP
<?php
|
|
/**
|
|
* Image Manager Class
|
|
*
|
|
* Handles image generation, variant management, and WordPress Media integration.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
* @since 0.1.0
|
|
*/
|
|
|
|
if (!defined("ABSPATH")) {
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Image Manager class.
|
|
*/
|
|
class WP_Agentic_Writer_Image_Manager
|
|
{
|
|
/**
|
|
* Singleton instance.
|
|
*
|
|
* @var WP_Agentic_Writer_Image_Manager
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Get singleton instance.
|
|
*
|
|
* @return WP_Agentic_Writer_Image_Manager
|
|
*/
|
|
public static function get_instance()
|
|
{
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
private function __construct()
|
|
{
|
|
// Private constructor for singleton.
|
|
}
|
|
|
|
/**
|
|
* Check if required tables exist.
|
|
*
|
|
* @since 0.1.0
|
|
* @return bool True if tables exist, false otherwise.
|
|
*/
|
|
public function tables_exist()
|
|
{
|
|
global $wpdb;
|
|
$table_images = $wpdb->prefix . "wpaw_images";
|
|
|
|
// Check if table exists using SHOW TABLES
|
|
$result = $wpdb->get_var("SHOW TABLES LIKE '{$table_images}'");
|
|
|
|
return $result === $table_images;
|
|
}
|
|
|
|
/**
|
|
* Ensure tables exist, create if missing.
|
|
*
|
|
* @since 0.1.0
|
|
* @return true|WP_Error True on success, WP_Error on failure.
|
|
*/
|
|
public function ensure_tables()
|
|
{
|
|
if (!$this->tables_exist()) {
|
|
$result = $this->create_tables();
|
|
if (!$result) {
|
|
return new WP_Error(
|
|
"table_creation_failed",
|
|
__(
|
|
"Failed to create image database tables. Please check database permissions.",
|
|
"wp-agentic-writer",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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", []);
|
|
$writing_model =
|
|
$settings["writing_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing");
|
|
|
|
$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 = [
|
|
[
|
|
"role" => "user",
|
|
"content" =>
|
|
"Analyze this article for image placement:\n\n" .
|
|
$article_markdown,
|
|
],
|
|
];
|
|
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
"planning",
|
|
);
|
|
$provider = $provider_result->provider;
|
|
$response = $provider->chat(
|
|
$messages,
|
|
["temperature" => 0.3],
|
|
"planning",
|
|
);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
// Extract JSON from response.
|
|
$json_match = [];
|
|
if (preg_match("/\{[\s\S]*\}/m", $response["content"], $json_match)) {
|
|
$placement_data = json_decode($json_match[0], true);
|
|
if (JSON_ERROR_NONE === json_last_error()) {
|
|
return $placement_data;
|
|
}
|
|
}
|
|
|
|
return new WP_Error(
|
|
"parse_error",
|
|
"Failed to parse placement analysis",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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", []);
|
|
$writing_model =
|
|
$settings["writing_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing");
|
|
$image_model =
|
|
$settings["image_model"] ??
|
|
WPAW_Model_Registry::get_default_model("image");
|
|
|
|
// 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 = wp_json_encode([
|
|
"article" => $article_markdown,
|
|
"placement_points" => $placement_data["image_placement_points"],
|
|
"image_count" => $placement_data["recommended_image_count"],
|
|
"target_image_model" => $image_model,
|
|
]);
|
|
|
|
$messages = [
|
|
[
|
|
"role" => "user",
|
|
"content" => "Generate image prompts:\n\n" . $user_input,
|
|
],
|
|
];
|
|
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
"planning",
|
|
);
|
|
$provider = $provider_result->provider;
|
|
$response = $provider->chat(
|
|
$messages,
|
|
["temperature" => 0.7],
|
|
"planning",
|
|
);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
// Extract JSON.
|
|
$json_match = [];
|
|
if (preg_match("/\{[\s\S]*\}/m", $response["content"], $json_match)) {
|
|
$image_specs = json_decode($json_match[0], true);
|
|
if (JSON_ERROR_NONE === json_last_error()) {
|
|
// 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.
|
|
*
|
|
* @param string $image_model Image model ID.
|
|
* @return array Model configuration.
|
|
*/
|
|
private function get_prompt_guidance_for_model($image_model)
|
|
{
|
|
$model_configs = [
|
|
"black-forest-labs/flux.2-klein" => [
|
|
"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" => [
|
|
"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" => [
|
|
"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.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param array $images Image specifications.
|
|
*/
|
|
private function save_image_recommendations($post_id, $images)
|
|
{
|
|
// Ensure tables exist before saving
|
|
$check = $this->ensure_tables();
|
|
if (is_wp_error($check)) {
|
|
error_log(
|
|
"WPAW Image Manager: Cannot save recommendations - tables not available",
|
|
);
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . "wpaw_images";
|
|
|
|
foreach ($images as $image_spec) {
|
|
$wpdb->insert(
|
|
$table,
|
|
[
|
|
"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"],
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save single image recommendation to database.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $agent_image_id Unique image identifier.
|
|
* @param string $placement Placement location.
|
|
* @param string $section_title Section title.
|
|
* @param string $prompt Image prompt/description.
|
|
* @param string $alt_text Alt text for image.
|
|
* @return int|false|WP_Error Insert ID, false on failure, or WP_Error if tables don't exist.
|
|
*/
|
|
public function save_image_recommendation(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$placement,
|
|
$section_title,
|
|
$prompt,
|
|
$alt_text,
|
|
) {
|
|
// Ensure tables exist before saving
|
|
$check = $this->ensure_tables();
|
|
if (is_wp_error($check)) {
|
|
return $check;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . "wpaw_images";
|
|
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
$image_model =
|
|
$settings["image_model"] ??
|
|
WPAW_Model_Registry::get_default_model("image");
|
|
|
|
$result = $wpdb->insert(
|
|
$table,
|
|
[
|
|
"post_id" => $post_id,
|
|
"agent_image_id" => $agent_image_id,
|
|
"placement" => $placement,
|
|
"section_title" => $section_title,
|
|
"prompt_initial" => $prompt,
|
|
"alt_text_initial" => $alt_text,
|
|
"image_model" => $image_model,
|
|
"status" => "pending",
|
|
],
|
|
["%d", "%s", "%s", "%s", "%s", "%s", "%s", "%s"],
|
|
);
|
|
|
|
if ($result) {
|
|
return $wpdb->insert_id;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get image recommendations for a post.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @return array|WP_Error Image recommendations or error if tables don't exist.
|
|
*/
|
|
public function get_image_recommendations($post_id)
|
|
{
|
|
// Ensure tables exist before querying
|
|
$check = $this->ensure_tables();
|
|
if (is_wp_error($check)) {
|
|
return $check;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . "wpaw_images";
|
|
|
|
$results = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$table} WHERE post_id = %d ORDER BY created_at ASC",
|
|
$post_id,
|
|
),
|
|
ARRAY_A,
|
|
);
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Generate image variants.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $agent_image_id Agent image ID.
|
|
* @param string $prompt Image prompt.
|
|
* @param int $variant_count Number of variants to generate.
|
|
* @return array|WP_Error Generated variants or error.
|
|
*/
|
|
public function generate_image_variants(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$prompt,
|
|
$variant_count = 2,
|
|
) {
|
|
// Ensure tables exist before proceeding
|
|
$check = $this->ensure_tables();
|
|
if (is_wp_error($check)) {
|
|
return $check;
|
|
}
|
|
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
"image",
|
|
);
|
|
$provider = $provider_result->provider;
|
|
|
|
$variants = [];
|
|
|
|
for ($i = 1; $i <= $variant_count; $i++) {
|
|
// Let the provider resolve its own model based on its settings by passing null
|
|
$result = $provider->generate_image($prompt, null, [
|
|
"size" => "1024x576",
|
|
"quality" => "hd",
|
|
"n" => 1,
|
|
]);
|
|
|
|
if (is_wp_error($result)) {
|
|
return $result;
|
|
}
|
|
|
|
// Download image to temp directory.
|
|
$temp_file = $this->download_temp_image(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$result["url"],
|
|
$i,
|
|
);
|
|
|
|
if (is_wp_error($temp_file)) {
|
|
return $temp_file;
|
|
}
|
|
|
|
// Save variant to database.
|
|
$variant_id = $this->save_variant(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$i,
|
|
$temp_file,
|
|
$prompt,
|
|
$result["model"] ?? "unknown",
|
|
$result,
|
|
);
|
|
|
|
if (class_exists("WP_Agentic_Writer_Cost_Tracker")) {
|
|
WP_Agentic_Writer_Cost_Tracker::get_instance()->record_usage_full(
|
|
$post_id,
|
|
$result["model"] ?? "unknown",
|
|
"image_generation",
|
|
(int) ($result["input_tokens"] ?? 0),
|
|
(int) ($result["output_tokens"] ?? 0),
|
|
(float) ($result["cost"] ?? 0),
|
|
$provider_result->actual_provider ?? "openrouter",
|
|
"",
|
|
"success",
|
|
);
|
|
}
|
|
|
|
$variants[] = [
|
|
"id" => $variant_id,
|
|
"variant_number" => $i,
|
|
"temp_file_url" => $temp_file["url"],
|
|
"cost" => $result["cost"],
|
|
"generation_time" => $result["generation_time"],
|
|
"image_model_used" => $image_model,
|
|
];
|
|
}
|
|
|
|
return $variants;
|
|
}
|
|
|
|
/**
|
|
* Download image to temp directory.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $agent_image_id Agent image ID.
|
|
* @param string $image_url Image URL.
|
|
* @param int $variant_number Variant number.
|
|
* @return array|WP_Error File info or error.
|
|
*/
|
|
private function download_temp_image(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$image_url,
|
|
$variant_number,
|
|
) {
|
|
$upload_dir = wp_upload_dir();
|
|
$temp_dir = $upload_dir["basedir"] . "/wpaw/" . $post_id;
|
|
|
|
if (!file_exists($temp_dir)) {
|
|
wp_mkdir_p($temp_dir);
|
|
}
|
|
|
|
$image_data = "";
|
|
$extension = "jpg";
|
|
|
|
if (
|
|
preg_match(
|
|
'#^data:image/([a-zA-Z0-9.+-]+);base64,(.+)$#',
|
|
(string) $image_url,
|
|
$matches,
|
|
)
|
|
) {
|
|
$extension = strtolower($matches[1]);
|
|
$extension = "jpeg" === $extension ? "jpg" : $extension;
|
|
$image_data = base64_decode($matches[2]);
|
|
if (false === $image_data) {
|
|
return new WP_Error(
|
|
"invalid_image_data",
|
|
__(
|
|
"Generated image data could not be decoded.",
|
|
"wp-agentic-writer",
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
// Download image.
|
|
$response = wp_remote_get($image_url, ["timeout" => 30]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
$image_data = wp_remote_retrieve_body($response);
|
|
|
|
// Determine file extension from content type.
|
|
$content_type = wp_remote_retrieve_header(
|
|
$response,
|
|
"content-type",
|
|
);
|
|
if (strpos($content_type, "png") !== false) {
|
|
$extension = "png";
|
|
} elseif (strpos($content_type, "webp") !== false) {
|
|
$extension = "webp";
|
|
}
|
|
}
|
|
|
|
$filename = sprintf(
|
|
"%s_variant_%d_%d.%s",
|
|
$agent_image_id,
|
|
$variant_number,
|
|
time(),
|
|
$extension,
|
|
);
|
|
|
|
$file_path = $temp_dir . "/" . $filename;
|
|
file_put_contents($file_path, $image_data);
|
|
|
|
$file_url =
|
|
$upload_dir["baseurl"] . "/wpaw/" . $post_id . "/" . $filename;
|
|
|
|
return [
|
|
"path" => $file_path,
|
|
"url" => $file_url,
|
|
"size" => filesize($file_path),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Save variant to database.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $agent_image_id Agent image ID.
|
|
* @param int $variant_number Variant number.
|
|
* @param array $temp_file Temp file info.
|
|
* @param string $prompt Prompt used.
|
|
* @param string $image_model Image model used.
|
|
* @param array $generation_result Generation result.
|
|
* @return int Variant ID.
|
|
*/
|
|
private function save_variant(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$variant_number,
|
|
$temp_file,
|
|
$prompt,
|
|
$image_model,
|
|
$generation_result,
|
|
) {
|
|
global $wpdb;
|
|
|
|
// Get agentic_image_id from wp_wpaw_images.
|
|
$table_images = $wpdb->prefix . "wpaw_images";
|
|
$agentic_image_id = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT id FROM {$table_images} WHERE post_id = %d AND agent_image_id = %s",
|
|
$post_id,
|
|
$agent_image_id,
|
|
),
|
|
);
|
|
|
|
$table_variants = $wpdb->prefix . "wpaw_images_variants";
|
|
|
|
$wpdb->insert(
|
|
$table_variants,
|
|
[
|
|
"agentic_image_id" => $agentic_image_id,
|
|
"post_id" => $post_id,
|
|
"agent_image_id" => $agent_image_id,
|
|
"variant_number" => $variant_number,
|
|
"temp_file_path" => $temp_file["path"],
|
|
"temp_file_url" => $temp_file["url"],
|
|
"file_size" => $temp_file["size"],
|
|
"prompt_used" => $prompt,
|
|
"image_model_used" => $image_model,
|
|
"generation_time" => $generation_result["generation_time"],
|
|
"cost" => $generation_result["cost"],
|
|
"status" => "temp",
|
|
],
|
|
[
|
|
"%d",
|
|
"%d",
|
|
"%s",
|
|
"%d",
|
|
"%s",
|
|
"%s",
|
|
"%d",
|
|
"%s",
|
|
"%s",
|
|
"%d",
|
|
"%f",
|
|
"%s",
|
|
],
|
|
);
|
|
|
|
return $wpdb->insert_id;
|
|
}
|
|
|
|
/**
|
|
* Commit image variant to WordPress Media Library.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $agent_image_id Agent image ID.
|
|
* @param int $variant_id Variant ID.
|
|
* @param string $alt_text Alt text.
|
|
* @return array|WP_Error Attachment info or error.
|
|
*/
|
|
public function commit_image_variant(
|
|
$post_id,
|
|
$agent_image_id,
|
|
$variant_id,
|
|
$alt_text,
|
|
) {
|
|
global $wpdb;
|
|
|
|
// Get variant info.
|
|
$table_variants = $wpdb->prefix . "wpaw_images_variants";
|
|
$variant = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$table_variants} WHERE id = %d",
|
|
$variant_id,
|
|
),
|
|
ARRAY_A,
|
|
);
|
|
|
|
if (!$variant) {
|
|
return new WP_Error("variant_not_found", "Variant not found");
|
|
}
|
|
|
|
if (
|
|
empty($variant["temp_file_path"]) ||
|
|
!file_exists($variant["temp_file_path"])
|
|
) {
|
|
return new WP_Error(
|
|
"variant_file_missing",
|
|
__(
|
|
"Generated image file is missing. Please generate the variant again.",
|
|
"wp-agentic-writer",
|
|
),
|
|
);
|
|
}
|
|
|
|
// Upload to Media Library.
|
|
require_once ABSPATH . "wp-admin/includes/image.php";
|
|
require_once ABSPATH . "wp-admin/includes/file.php";
|
|
require_once ABSPATH . "wp-admin/includes/media.php";
|
|
|
|
$sideload_tmp = wp_tempnam(basename($variant["temp_file_path"]));
|
|
if (
|
|
!$sideload_tmp ||
|
|
!copy($variant["temp_file_path"], $sideload_tmp)
|
|
) {
|
|
return new WP_Error(
|
|
"variant_copy_failed",
|
|
__(
|
|
"Generated image could not be prepared for upload.",
|
|
"wp-agentic-writer",
|
|
),
|
|
);
|
|
}
|
|
|
|
$file_array = [
|
|
"name" => basename($variant["temp_file_path"]),
|
|
"tmp_name" => $sideload_tmp,
|
|
];
|
|
|
|
$attachment_id = media_handle_sideload($file_array, $post_id);
|
|
|
|
if (is_wp_error($attachment_id)) {
|
|
if (file_exists($sideload_tmp)) {
|
|
@unlink($sideload_tmp);
|
|
}
|
|
return $attachment_id;
|
|
}
|
|
|
|
// Set alt text.
|
|
update_post_meta(
|
|
$attachment_id,
|
|
"_wp_attachment_image_alt",
|
|
sanitize_text_field($alt_text),
|
|
);
|
|
|
|
// Update wp_wpaw_images table.
|
|
$table_images = $wpdb->prefix . "wpaw_images";
|
|
$wpdb->update(
|
|
$table_images,
|
|
[
|
|
"attachment_id" => $attachment_id,
|
|
"status" => "committed",
|
|
],
|
|
[
|
|
"post_id" => $post_id,
|
|
"agent_image_id" => $agent_image_id,
|
|
],
|
|
["%d", "%s"],
|
|
["%d", "%s"],
|
|
);
|
|
|
|
// Mark variant as selected.
|
|
$wpdb->update(
|
|
$table_variants,
|
|
[
|
|
"is_selected" => 1,
|
|
"selected_at" => current_time("mysql"),
|
|
"status" => "selected",
|
|
],
|
|
["id" => $variant_id],
|
|
["%d", "%s", "%s"],
|
|
["%d"],
|
|
);
|
|
|
|
$attachment_url = wp_get_attachment_url($attachment_id);
|
|
|
|
return [
|
|
"attachment_id" => $attachment_id,
|
|
"attachment_url" => $attachment_url,
|
|
"alt" => $alt_text,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Cleanup old temp images (7+ days old).
|
|
*/
|
|
public function cleanup_old_temp_images()
|
|
{
|
|
global $wpdb;
|
|
|
|
$table_variants = $wpdb->prefix . "wpaw_images_variants";
|
|
|
|
// Get temp images older than 7 days.
|
|
$old_variants = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$table_variants}
|
|
WHERE status = 'temp'
|
|
AND created_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
|
|
7,
|
|
),
|
|
ARRAY_A,
|
|
);
|
|
|
|
foreach ($old_variants as $variant) {
|
|
// Delete file.
|
|
if (file_exists($variant["temp_file_path"])) {
|
|
unlink($variant["temp_file_path"]);
|
|
}
|
|
|
|
// Update status.
|
|
$wpdb->update(
|
|
$table_variants,
|
|
[
|
|
"status" => "auto_deleted",
|
|
"deleted_at" => current_time("mysql"),
|
|
],
|
|
["id" => $variant["id"]],
|
|
["%s", "%s"],
|
|
["%d"],
|
|
);
|
|
}
|
|
}
|
|
}
|