| 1 |
<?php |
| 2 |
|
| 3 |
namespace Better_Payment\Lite\AI\Services; |
| 4 |
|
| 5 |
use Better_Payment\Lite\AI\AIManager; |
| 6 |
|
| 7 |
if ( ! defined( 'ABSPATH' ) ) { |
| 8 |
exit; |
| 9 |
} |
| 10 |
|
| 11 |
/** |
| 12 |
* AI image generation → WordPress Media Library. |
| 13 |
* |
| 14 |
* Flow: prompt → active provider->generate_image() → receive URL or base64 → |
| 15 |
* persist as a real WP attachment → return the photo-ready shape the builder's |
| 16 |
* `photo` element and `replace_image` operation expect. |
| 17 |
* |
| 18 |
* The returned { id, url, sizes, alt } maps directly onto the photo element's |
| 19 |
* { src_id, src, src_sizes, alt } settings. |
| 20 |
*/ |
| 21 |
class ImageGenerator { |
| 22 |
|
| 23 |
/** |
| 24 |
* Generate an image and add it to the media library. |
| 25 |
* |
| 26 |
* @param string $prompt |
| 27 |
* @param int $attach_to_post Optional parent post id. |
| 28 |
* @return array|\WP_Error { id, url, sizes, alt } |
| 29 |
*/ |
| 30 |
public static function generate( string $prompt, int $attach_to_post = 0 ) { |
| 31 |
$provider = AIManager::active_provider(); |
| 32 |
if ( null === $provider ) { |
| 33 |
return new \WP_Error( 'ai_no_provider', __( 'No AI provider is configured.', 'better-payment' ), [ 'status' => 400 ] ); |
| 34 |
} |
| 35 |
if ( ! $provider->supports_images() ) { |
| 36 |
return new \WP_Error( 'ai_no_images', __( 'The selected AI provider cannot generate images. Choose OpenAI or Gemini under Settings → AI.', 'better-payment' ), [ 'status' => 400 ] ); |
| 37 |
} |
| 38 |
if ( ! $provider->is_configured() ) { |
| 39 |
return new \WP_Error( 'ai_not_configured', __( 'The selected AI provider is missing its API key.', 'better-payment' ), [ 'status' => 400 ] ); |
| 40 |
} |
| 41 |
|
| 42 |
$image = $provider->generate_image( $prompt ); |
| 43 |
if ( ! empty( $image['error'] ) ) { |
| 44 |
return new \WP_Error( 'ai_image_error', (string) $image['error'], [ 'status' => 502 ] ); |
| 45 |
} |
| 46 |
|
| 47 |
$saved = null; |
| 48 |
if ( ! empty( $image['b64'] ) ) { |
| 49 |
$saved = self::sideload_bytes( base64_decode( $image['b64'] ), (string) ( $image['mime'] ?? 'image/png' ), $prompt, $attach_to_post ); |
| 50 |
} elseif ( ! empty( $image['url'] ) ) { |
| 51 |
$saved = self::sideload_url( (string) $image['url'], $prompt, $attach_to_post ); |
| 52 |
} |
| 53 |
|
| 54 |
if ( is_wp_error( $saved ) ) { |
| 55 |
return $saved; |
| 56 |
} |
| 57 |
|
| 58 |
$attachment_id = (int) $saved; |
| 59 |
if ( ! $attachment_id ) { |
| 60 |
return new \WP_Error( 'ai_image_save_failed', __( 'Could not save the generated image.', 'better-payment' ), [ 'status' => 500 ] ); |
| 61 |
} |
| 62 |
|
| 63 |
return self::attachment_payload( $attachment_id, $prompt ); |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Persist raw image bytes as an attachment. |
| 68 |
* |
| 69 |
* @return int|\WP_Error |
| 70 |
*/ |
| 71 |
private static function sideload_bytes( string $bytes, string $mime, string $prompt, int $parent ) { |
| 72 |
self::require_media(); |
| 73 |
|
| 74 |
$ext = 'image/jpeg' === $mime ? 'jpg' : 'png'; |
| 75 |
$filename = 'bp-ai-' . gmdate( 'YmdHis' ) . '-' . wp_generate_password( 6, false ) . '.' . $ext; |
| 76 |
$upload = wp_upload_bits( $filename, null, $bytes ); |
| 77 |
|
| 78 |
if ( ! empty( $upload['error'] ) ) { |
| 79 |
return new \WP_Error( 'ai_image_upload_failed', (string) $upload['error'], [ 'status' => 500 ] ); |
| 80 |
} |
| 81 |
|
| 82 |
$attachment = [ |
| 83 |
'post_mime_type' => $mime, |
| 84 |
'post_title' => self::title_from_prompt( $prompt ), |
| 85 |
'post_content' => '', |
| 86 |
'post_status' => 'inherit', |
| 87 |
]; |
| 88 |
// wp_insert_attachment() returns 0 on failure (no $wp_error flag passed). |
| 89 |
$attach_id = wp_insert_attachment( $attachment, $upload['file'], $parent ); |
| 90 |
if ( ! $attach_id ) { |
| 91 |
return new \WP_Error( 'ai_image_insert_failed', __( 'Could not create the image attachment.', 'better-payment' ), [ 'status' => 500 ] ); |
| 92 |
} |
| 93 |
$meta = wp_generate_attachment_metadata( $attach_id, $upload['file'] ); |
| 94 |
wp_update_attachment_metadata( $attach_id, $meta ); |
| 95 |
|
| 96 |
return (int) $attach_id; |
| 97 |
} |
| 98 |
|
| 99 |
/** |
| 100 |
* Download a remote image URL into the media library. |
| 101 |
* |
| 102 |
* @return int|\WP_Error |
| 103 |
*/ |
| 104 |
private static function sideload_url( string $url, string $prompt, int $parent ) { |
| 105 |
self::require_media(); |
| 106 |
$attach_id = media_sideload_image( $url, $parent, self::title_from_prompt( $prompt ), 'id' ); |
| 107 |
return is_wp_error( $attach_id ) ? $attach_id : (int) $attach_id; |
| 108 |
} |
| 109 |
|
| 110 |
/** |
| 111 |
* Build the photo-ready payload from a saved attachment. |
| 112 |
* |
| 113 |
* @return array{ id: int, url: string, sizes: array, alt: string } |
| 114 |
*/ |
| 115 |
private static function attachment_payload( int $attachment_id, string $prompt ): array { |
| 116 |
$alt = self::title_from_prompt( $prompt ); |
| 117 |
update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt ); |
| 118 |
|
| 119 |
$meta = wp_get_attachment_metadata( $attachment_id ); |
| 120 |
$sizes = []; |
| 121 |
if ( is_array( $meta ) && ! empty( $meta['sizes'] ) ) { |
| 122 |
foreach ( array_keys( $meta['sizes'] ) as $size ) { |
| 123 |
$src = wp_get_attachment_image_src( $attachment_id, $size ); |
| 124 |
if ( $src ) { |
| 125 |
$sizes[ $size ] = [ 'url' => $src[0], 'width' => $src[1], 'height' => $src[2] ]; |
| 126 |
} |
| 127 |
} |
| 128 |
} |
| 129 |
|
| 130 |
return [ |
| 131 |
'id' => $attachment_id, |
| 132 |
'url' => (string) wp_get_attachment_url( $attachment_id ), |
| 133 |
'sizes' => $sizes, |
| 134 |
'alt' => $alt, |
| 135 |
]; |
| 136 |
} |
| 137 |
|
| 138 |
private static function title_from_prompt( string $prompt ): string { |
| 139 |
$title = wp_trim_words( sanitize_text_field( $prompt ), 10, '' ); |
| 140 |
return '' !== $title ? $title : __( 'AI generated image', 'better-payment' ); |
| 141 |
} |
| 142 |
|
| 143 |
private static function require_media(): void { |
| 144 |
if ( ! function_exists( 'media_sideload_image' ) ) { |
| 145 |
require_once ABSPATH . 'wp-admin/includes/media.php'; |
| 146 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 147 |
require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 148 |
} |
| 149 |
} |
| 150 |
} |
| 151 |
|