Skip to content

Commit af5e00d

Browse files
authored
feat: Implement DALL-E image generation for OpenAI Bridge (#178)
* Implement Dall-E image generation * Adjust some testcases for styling * Rename GeneratedImagesResponse to ImagesResponse * Some review adjustments
1 parent 7779a13 commit af5e00d

13 files changed

+488
-2
lines changed

examples/image-generator-dall-e-2.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE;
4+
use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory;
5+
use Symfony\Component\Dotenv\Dotenv;
6+
7+
require_once dirname(__DIR__).'/vendor/autoload.php';
8+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
9+
10+
if (empty($_ENV['OPENAI_API_KEY'])) {
11+
echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL;
12+
exit(1);
13+
}
14+
15+
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
16+
17+
$response = $platform->request(
18+
model: new DallE(),
19+
input: 'A cartoon-style elephant with a long trunk and large ears.',
20+
options: [
21+
'version' => DallE::DALL_E_2, // Utilize Dall-E 2 version
22+
'response_format' => 'url', // Generate response as URL
23+
'n' => 2, // Generate multiple images for example
24+
],
25+
);
26+
27+
foreach ($response->getContent() as $index => $image) {
28+
echo 'Image '.$index.': '.$image->url.PHP_EOL;
29+
}

examples/image-generator-dall-e-3.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE;
4+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\ImageResponse;
5+
use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory;
6+
use PhpLlm\LlmChain\Model\Response\AsyncResponse;
7+
use Symfony\Component\Dotenv\Dotenv;
8+
9+
require_once dirname(__DIR__).'/vendor/autoload.php';
10+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
11+
12+
if (empty($_ENV['OPENAI_API_KEY'])) {
13+
echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL;
14+
exit(1);
15+
}
16+
17+
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
18+
19+
$response = $platform->request(
20+
model: new DallE(),
21+
input: 'A cartoon-style elephant with a long trunk and large ears.',
22+
options: [
23+
'version' => DallE::DALL_E_3, // Utilize Dall-E 3 version
24+
],
25+
);
26+
27+
if ($response instanceof AsyncResponse) {
28+
$response = $response->unwrap();
29+
}
30+
31+
assert($response instanceof ImageResponse);
32+
33+
echo 'Revised Prompt: '.$response->revisedPrompt.PHP_EOL.PHP_EOL;
34+
35+
foreach ($response->getContent() as $index => $image) {
36+
echo 'Image '.$index.': '.$image->url.PHP_EOL;
37+
}

src/Bridge/OpenAI/DallE.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI;
6+
7+
use PhpLlm\LlmChain\Model\Model;
8+
9+
final readonly class DallE implements Model
10+
{
11+
public const DALL_E_2 = 'dall-e-2';
12+
public const DALL_E_3 = 'dall-e-3';
13+
14+
/** @param array<string, mixed> $options The default options for the model usage */
15+
public function __construct(
16+
private string $version = self::DALL_E_2,
17+
private array $options = [],
18+
) {
19+
}
20+
21+
public function getVersion(): string
22+
{
23+
return $this->version;
24+
}
25+
26+
/** @return array<string, mixed> */
27+
public function getOptions(): array
28+
{
29+
return $this->options;
30+
}
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\DallE;
6+
7+
use Webmozart\Assert\Assert;
8+
9+
final readonly class Base64Image
10+
{
11+
public function __construct(
12+
public string $encodedImage,
13+
) {
14+
Assert::stringNotEmpty($encodedImage, 'The base64 encoded image generated must be given.');
15+
}
16+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\DallE;
6+
7+
use PhpLlm\LlmChain\Model\Response\ResponseInterface;
8+
9+
class ImageResponse implements ResponseInterface
10+
{
11+
/** @var list<Base64Image|UrlImage> */
12+
private readonly array $images;
13+
14+
public function __construct(
15+
public ?string $revisedPrompt = null, // Only string on Dall-E 3 usage
16+
Base64Image|UrlImage ...$images,
17+
) {
18+
$this->images = \array_values($images);
19+
}
20+
21+
/**
22+
* @return list<Base64Image|UrlImage>
23+
*/
24+
public function getContent(): array
25+
{
26+
return $this->images;
27+
}
28+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\DallE;
6+
7+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE;
8+
use PhpLlm\LlmChain\Model\Model;
9+
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
10+
use PhpLlm\LlmChain\Platform\ModelClient as PlatformResponseFactory;
11+
use PhpLlm\LlmChain\Platform\ResponseConverter as PlatformResponseConverter;
12+
use Symfony\Contracts\HttpClient\HttpClientInterface;
13+
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
14+
use Webmozart\Assert\Assert;
15+
16+
/**
17+
* @see https://platform.openai.com/docs/api-reference/images/create
18+
*/
19+
final readonly class ModelClient implements PlatformResponseFactory, PlatformResponseConverter
20+
{
21+
public function __construct(
22+
private HttpClientInterface $httpClient,
23+
#[\SensitiveParameter]
24+
private string $apiKey,
25+
) {
26+
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
27+
Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".');
28+
}
29+
30+
public function supports(Model $model, array|string|object $input): bool
31+
{
32+
return $model instanceof DallE;
33+
}
34+
35+
public function request(Model $model, object|array|string $input, array $options = []): HttpResponse
36+
{
37+
return $this->httpClient->request('POST', 'https://api.openai.com/v1/images/generations', [
38+
'auth_bearer' => $this->apiKey,
39+
'json' => \array_merge($options, [
40+
'model' => $model->getVersion(),
41+
'prompt' => $input,
42+
]),
43+
]);
44+
}
45+
46+
public function convert(HttpResponse $response, array $options = []): LlmResponse
47+
{
48+
$response = $response->toArray();
49+
if (!isset($response['data'][0])) {
50+
throw new \RuntimeException('No image generated.');
51+
}
52+
53+
$images = [];
54+
foreach ($response['data'] as $image) {
55+
if ('url' === $options['response_format']) {
56+
$images[] = new UrlImage($image['url']);
57+
58+
continue;
59+
}
60+
61+
$images[] = new Base64Image($image['b64_json']);
62+
}
63+
64+
return new ImageResponse($image['revised_prompt'] ?? null, ...$images);
65+
}
66+
}

src/Bridge/OpenAI/DallE/UrlImage.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\DallE;
6+
7+
use Webmozart\Assert\Assert;
8+
9+
final readonly class UrlImage
10+
{
11+
public function __construct(
12+
public string $url,
13+
) {
14+
Assert::stringNotEmpty($url, 'The image url must be given.');
15+
}
16+
}

src/Bridge/OpenAI/PlatformFactory.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpLlm\LlmChain\Bridge\OpenAI;
66

7+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\ModelClient as DallEModelClient;
78
use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings\ModelClient as EmbeddingsModelClient;
89
use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
910
use PhpLlm\LlmChain\Bridge\OpenAI\GPT\ModelClient as GPTModelClient;
@@ -21,9 +22,19 @@ public static function create(
2122
): Platform {
2223
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
2324

25+
$dallEModelClient = new DallEModelClient($httpClient, $apiKey);
26+
2427
return new Platform(
25-
[new GPTModelClient($httpClient, $apiKey), new EmbeddingsModelClient($httpClient, $apiKey)],
26-
[new GPTResponseConverter(), new EmbeddingsResponseConverter()],
28+
[
29+
new GPTModelClient($httpClient, $apiKey),
30+
new EmbeddingsModelClient($httpClient, $apiKey),
31+
$dallEModelClient,
32+
],
33+
[
34+
new GPTResponseConverter(),
35+
new EmbeddingsResponseConverter(),
36+
$dallEModelClient,
37+
],
2738
);
2839
}
2940
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Tests\Bridge\OpenAI\DallE;
6+
7+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\Base64Image;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\Attributes\Small;
10+
use PHPUnit\Framework\Attributes\Test;
11+
use PHPUnit\Framework\TestCase;
12+
13+
#[CoversClass(Base64Image::class)]
14+
#[Small]
15+
final class Base64ImageTest extends TestCase
16+
{
17+
#[Test]
18+
public function itCreatesBase64Image(): void
19+
{
20+
$emptyPixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
21+
$base64Image = new Base64Image($emptyPixel);
22+
23+
self::assertSame($emptyPixel, $base64Image->encodedImage);
24+
}
25+
26+
#[Test]
27+
public function itThrowsExceptionWhenBase64ImageIsEmpty(): void
28+
{
29+
$this->expectException(\InvalidArgumentException::class);
30+
$this->expectExceptionMessage('The base64 encoded image generated must be given.');
31+
32+
new Base64Image('');
33+
}
34+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Tests\Bridge\OpenAI\DallE;
6+
7+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\Base64Image;
8+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\ImageResponse;
9+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\UrlImage;
10+
use PHPUnit\Framework\Attributes\CoversClass;
11+
use PHPUnit\Framework\Attributes\Small;
12+
use PHPUnit\Framework\Attributes\Test;
13+
use PHPUnit\Framework\Attributes\UsesClass;
14+
use PHPUnit\Framework\TestCase;
15+
16+
#[CoversClass(ImageResponse::class)]
17+
#[UsesClass(Base64Image::class)]
18+
#[UsesClass(UrlImage::class)]
19+
#[Small]
20+
final class ImageResponseTest extends TestCase
21+
{
22+
#[Test]
23+
public function itCreatesImagesResponse(): void
24+
{
25+
$base64Image = new Base64Image('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
26+
$generatedImagesResponse = new ImageResponse(null, $base64Image);
27+
28+
self::assertNull($generatedImagesResponse->revisedPrompt);
29+
self::assertCount(1, $generatedImagesResponse->getContent());
30+
self::assertSame($base64Image, $generatedImagesResponse->getContent()[0]);
31+
}
32+
33+
#[Test]
34+
public function itCreatesImagesResponseWithRevisedPrompt(): void
35+
{
36+
$base64Image = new Base64Image('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
37+
$generatedImagesResponse = new ImageResponse('revised prompt', $base64Image);
38+
39+
self::assertSame('revised prompt', $generatedImagesResponse->revisedPrompt);
40+
self::assertCount(1, $generatedImagesResponse->getContent());
41+
self::assertSame($base64Image, $generatedImagesResponse->getContent()[0]);
42+
}
43+
44+
#[Test]
45+
public function itIsCreatableWithMultipleImages(): void
46+
{
47+
$image1 = new UrlImage('https://example');
48+
$image2 = new UrlImage('https://example2');
49+
50+
$generatedImagesResponse = new ImageResponse(null, $image1, $image2);
51+
52+
self::assertCount(2, $generatedImagesResponse->getContent());
53+
self::assertSame($image1, $generatedImagesResponse->getContent()[0]);
54+
self::assertSame($image2, $generatedImagesResponse->getContent()[1]);
55+
}
56+
}

0 commit comments

Comments
 (0)