submit_to_queue( $api_key, $model, $full_prompt ); if ( ! $request_id ) { return null; } // Step 2: Poll for result. $image_url = $this->poll_for_result( $api_key, $model, $request_id ); if ( ! $image_url ) { return null; } // Step 3: Download and upload to WP media library. $attachment_id = $this->upload_to_media( $image_url, $post_title, $image_alt, $image_caption ); if ( $attachment_id ) { /* translators: 1: attachment ID, 2: post title */ Logger::info( sprintf( __( 'Image generated and uploaded (ID: %1$d) for "%2$s".', 'ai-content-autopilot' ), $attachment_id, $post_title ) ); } return $attachment_id; } /** * Submit image generation request to fal.ai queue. * * @return string|null Request ID or null on failure. */ private function submit_to_queue( $api_key, $model, $prompt ) { $response = wp_remote_post( "https://queue.fal.run/{$model}", array( 'timeout' => 30, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Key ' . $api_key, ), 'body' => wp_json_encode( array( 'prompt' => $prompt, 'image_size' => 'landscape_16_9', 'width' => 1280, 'height' => 720, 'aspect_ratio' => '16:9', 'num_images' => 1, ) ), ) ); if ( is_wp_error( $response ) ) { Logger::error( __( 'fal.ai queue error: ', 'ai-content-autopilot' ) . $response->get_error_message() ); return null; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); $request_id = $data['request_id'] ?? null; if ( ! $request_id ) { Logger::error( __( 'Did not receive request_id from fal.ai.', 'ai-content-autopilot' ), $data ); return null; } return $request_id; } /** * Poll fal.ai queue for completion. * * @return string|null Image URL or null on failure/timeout. */ private function poll_for_result( $api_key, $model, $request_id ) { for ( $i = 0; $i < self::MAX_POLLS; $i++ ) { sleep( self::POLL_INTERVAL ); $response = wp_remote_get( "https://queue.fal.run/{$model}/requests/{$request_id}/status", array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Key ' . $api_key, ), ) ); if ( is_wp_error( $response ) ) { continue; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); $status = $data['status'] ?? ''; if ( $status === 'COMPLETED' ) { // Fetch the result. $result_response = wp_remote_get( "https://queue.fal.run/{$model}/requests/{$request_id}", array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Key ' . $api_key, ), ) ); if ( is_wp_error( $result_response ) ) { Logger::error( __( 'Could not fetch fal.ai result: ', 'ai-content-autopilot' ) . $result_response->get_error_message() ); return null; } $result = json_decode( wp_remote_retrieve_body( $result_response ), true ); return $result['images'][0]['url'] ?? null; } if ( $status === 'FAILED' ) { Logger::error( __( 'fal.ai image generation failed.', 'ai-content-autopilot' ), $data ); return null; } } /* translators: %d: number of poll attempts */ Logger::error( sprintf( __( 'fal.ai timeout after %d polls.', 'ai-content-autopilot' ), self::MAX_POLLS ) ); return null; } /** * Convert a temporary image file to WebP format. * * @param string $tmp_path Path to temporary image file. * @return string Original path if conversion fails, new .webp path on success. */ public static function convert_to_webp( $tmp_path ) { if ( ! function_exists( 'imagewebp' ) ) { return $tmp_path; } $image = @imagecreatefromstring( file_get_contents( $tmp_path ) ); if ( ! $image ) { return $tmp_path; } $webp_path = $tmp_path . '.webp'; $success = @imagewebp( $image, $webp_path, 82 ); imagedestroy( $image ); if ( ! $success || ! file_exists( $webp_path ) ) { return $tmp_path; } wp_delete_file( $tmp_path ); return $webp_path; } /** * Download an image from URL and upload to WordPress media library. * * @param string $url Image URL. * @param string $post_title Post title for naming. * @param string $image_alt SEO alt text. * @param string $image_caption Caption text. * @return int|null Attachment ID. */ private function upload_to_media( $url, $post_title, $image_alt = '', $image_caption = '' ) { if ( ! function_exists( 'media_sideload_image' ) ) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; } // Download to temp file. $tmp = download_url( $url, 60 ); if ( is_wp_error( $tmp ) ) { Logger::error( __( 'Could not download image: ', 'ai-content-autopilot' ) . $tmp->get_error_message() ); return null; } // Convert to WebP for smaller file sizes. $tmp = self::convert_to_webp( $tmp ); $ext = ( substr( $tmp, -5 ) === '.webp' ) ? '.webp' : '.png'; $filename = sanitize_file_name( sanitize_title( $post_title ) ) . $ext; $file_array = array( 'name' => $filename, 'tmp_name' => $tmp, ); $attachment_id = media_handle_sideload( $file_array, 0, $post_title ); if ( is_wp_error( $attachment_id ) ) { wp_delete_file( $tmp ); Logger::error( __( 'Could not upload image to media library: ', 'ai-content-autopilot' ) . $attachment_id->get_error_message() ); return null; } // SEO: Set alt text (uses AI-generated alt, falls back to post title). $alt = ! empty( $image_alt ) ? sanitize_text_field( $image_alt ) : sanitize_text_field( $post_title ); update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt ); // SEO: Set caption and description on the attachment post. $caption_text = ! empty( $image_caption ) ? sanitize_text_field( $image_caption ) : ''; wp_update_post( array( 'ID' => $attachment_id, 'post_excerpt' => $caption_text, // Caption 'post_content' => sanitize_text_field( $post_title ), // Description 'post_title' => $alt, // Title ) ); return $attachment_id; } }