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, " 'user', 'content' => "Analyze this article for image placement:\n\n" . $article_markdown, ), ); $provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'planning' ); $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_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', 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 = wp_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_Provider_Manager::get_provider_for_task( 'planning' ); $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_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 = 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. * * @param int $post_id Post ID. * @param array $images Image specifications. */ 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' ) ); } } /** * 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 Insert ID or false on failure. */ public function save_image_recommendation( $post_id, $agent_image_id, $placement, $section_title, $prompt, $alt_text ) { global $wpdb; $table = $wpdb->prefix . 'wpaw_images'; $settings = get_option( 'wp_agentic_writer_settings', array() ); $image_model = $settings['image_model'] ?? 'openai/gpt-4o'; $result = $wpdb->insert( $table, array( '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', ), array( '%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 Image recommendations. */ public function get_image_recommendations( $post_id ) { 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 ) { $settings = get_option( 'wp_agentic_writer_settings', array() ); $image_model = $settings['image_model'] ?? 'openai/gpt-4o'; $provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'image' ); $variants = array(); for ( $i = 1; $i <= $variant_count; $i++ ) { $result = $provider->generate_image( $prompt, $image_model, array( '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, $image_model, $result ); $variants[] = array( '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 ); } // Download image. $response = wp_remote_get( $image_url, array( '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' ); $extension = 'jpg'; if ( strpos( $content_type, 'png' ) !== false ) { $extension = 'png'; } $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 array( '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, array( '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', ), array( '%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' ); } // 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'; $file_array = array( 'name' => basename( $variant['temp_file_path'] ), 'tmp_name' => $variant['temp_file_path'], ); $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', sanitize_text_field( $alt_text ) ); // Update wp_wpaw_images table. $table_images = $wpdb->prefix . 'wpaw_images'; $wpdb->update( $table_images, array( 'attachment_id' => $attachment_id, 'status' => 'committed', ), array( 'post_id' => $post_id, 'agent_image_id' => $agent_image_id, ), array( '%d', '%s' ), array( '%d', '%s' ) ); // Mark variant as selected. $wpdb->update( $table_variants, array( 'is_selected' => 1, 'selected_at' => current_time( 'mysql' ), 'status' => 'selected', ), array( 'id' => $variant_id ), array( '%d', '%s', '%s' ), array( '%d' ) ); $attachment_url = wp_get_attachment_url( $attachment_id ); return array( '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, array( 'status' => 'auto_deleted', 'deleted_at' => current_time( 'mysql' ), ), array( 'id' => $variant['id'] ), array( '%s', '%s' ), array( '%d' ) ); } } }