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, " "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"], ); } } }