provider = $provider ?? new PRAutoBlogger_Cloudflare_Image_Provider(); $this->prompt_builder = new PRAutoBlogger_Image_Prompt_Builder(); $this->sideloader = new PRAutoBlogger_Image_Media_Sideloader(); $this->cost_tracker = $cost_tracker ?? new PRAutoBlogger_Cost_Tracker(); } /** * Generate and attach images to a published post. * * Generates Image A (article-driven) and Image B (source-driven). If enabled * in settings. Each image is independently fallible. * * @param int $post_id Post ID to attach images to. * @param array{ * post_title?: string, * post_content?: string, * suggested_title?: string, * } $article_data Article title + content. * @param array{ * title?: string, * selftext?: string, * comments?: string[], * }|null $source_data Optional Reddit source data for Image B. * * @return array{ * image_a_id?: int, * image_b_id?: int, * cost_usd: float, * errors: string[], * } Attachment IDs (if successful) and cost/errors. */ public function generate_and_attach_images( int $post_id, array $article_data, ?array $source_data = null ): array { $result = [ 'cost_usd' => 0.0, 'errors' => [], ]; // Early exit if image generation is disabled. if ( ! get_option( 'prautoblogger_image_enabled' ) ) { PRAutoBlogger_Logger::instance()->info( 'Image generation disabled in settings.', 'image_pipeline' ); return $result; } // Generate Image A (article-driven prompt). $image_a_result = $this->generate_image_a( $post_id, $article_data ); if ( ! is_wp_error( $image_a_result ) && isset( $image_a_result['attachment_id'] ) ) { $result['image_a_id'] = $image_a_result['attachment_id']; } else { $result['errors'][] = is_wp_error( $image_a_result ) ? $image_a_result->get_error_message() : 'Image A generation produced no attachment ID.'; } if ( ! is_wp_error( $image_a_result ) ) { $result['cost_usd'] += $image_a_result['cost_usd'] ?? 0.0; } // Generate Image B (source-driven prompt) if source data is available. if ( null !== $source_data && ! empty( $source_data ) ) { $image_b_result = $this->generate_image_b( $post_id, $source_data ); if ( ! is_wp_error( $image_b_result ) && isset( $image_b_result['attachment_id'] ) ) { $result['image_b_id'] = $image_b_result['attachment_id']; } else { $result['errors'][] = is_wp_error( $image_b_result ) ? $image_b_result->get_error_message() : 'Image B generation produced no attachment ID.'; } if ( ! is_wp_error( $image_b_result ) ) { $result['cost_usd'] += $image_b_result['cost_usd'] ?? 0.0; } } return $result; } /** * Generate Image A from article content. * * @param int $post_id Post ID. * @param array $article_data Article data. * * @return array{ * attachment_id?: int, * cost_usd: float, * }|\WP_Error */ private function generate_image_a( int $post_id, array $article_data ) { try { // Build the article-driven prompt. $prompt = $this->prompt_builder->build_article_prompt( $article_data ); // Estimate cost before generating. $estimated_cost = $this->provider->estimate_cost( self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT ); if ( $this->cost_tracker->would_exceed_budget( $estimated_cost ) ) { return new \WP_Error( 'budget_exceeded', 'Image generation would exceed monthly budget.' ); } // Generate the image. $image_data = $this->provider->generate_image( $prompt, self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT ); // Sideload the image into media library. $attachment_id = $this->sideloader->sideload_image( $image_data, $post_id, substr( $prompt, 0, 100 ) // Use first 100 chars of prompt as alt text. ); if ( is_wp_error( $attachment_id ) ) { return $attachment_id; } // Log the cost. $this->cost_tracker->log_image_generation( $image_data['cost_usd'], 'flux-1-schnell', $post_id, 'image_a' ); PRAutoBlogger_Logger::instance()->info( sprintf( 'Image A generated for post %d (attachment %d, cost $%.4f)', $post_id, $attachment_id, $image_data['cost_usd'] ), 'image_pipeline' ); return [ 'attachment_id' => $attachment_id, 'cost_usd' => $image_data['cost_usd'], ]; } catch ( \Exception $e ) { PRAutoBlogger_Logger::instance()->error( 'Image A generation failed: ' . $e->getMessage(), 'image_pipeline' ); return new \WP_Error( 'image_generation_failed', $e->getMessage() ); } } /** * Generate Image B from source data. * * @param int $post_id Post ID. * @param array $source_data Source Reddit data. * * @return array{ * attachment_id?: int, * cost_usd: float, * }|\WP_Error */ private function generate_image_b( int $post_id, array $source_data ) { try { // Build the source-driven prompt. $prompt = $this->prompt_builder->build_source_prompt( $source_data ); // Estimate cost before generating. $estimated_cost = $this->provider->estimate_cost( self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT ); if ( $this->cost_tracker->would_exceed_budget( $estimated_cost ) ) { return new \WP_Error( 'budget_exceeded', 'Image generation would exceed monthly budget.' ); } // Generate the image. $image_data = $this->provider->generate_image( $prompt, self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT ); // Sideload the image into media library. $attachment_id = $this->sideloader->sideload_image( $image_data, $post_id, substr( $prompt, 0, 100 ) // Use first 100 chars of prompt as alt text. ); if ( is_wp_error( $attachment_id ) ) { return $attachment_id; } // Log the cost. $this->cost_tracker->log_image_generation( $image_data['cost_usd'], 'flux-1-schnell', $post_id, 'image_b' ); PRAutoBlogger_Logger::instance()->info( sprintf( 'Image B generated for post %d (attachment %d, cost $%.4f)', $post_id, $attachment_id, $image_data['cost_usd'] ), 'image_pipeline' ); return [ 'attachment_id' => $attachment_id, 'cost_usd' => $image_data['cost_usd'], ]; } catch ( \Exception $e ) { PRAutoBlogger_Logger::instance()->error( 'Image B generation failed: ' . $e->getMessage(), 'image_pipeline' ); return new \WP_Error( 'image_generation_failed', $e->getMessage() ); } } }