diff --git a/examples/image-generator-dall-e-2.php b/examples/image-generator-dall-e-2.php new file mode 100644 index 0000000..7185a44 --- /dev/null +++ b/examples/image-generator-dall-e-2.php @@ -0,0 +1,26 @@ +loadEnv(dirname(__DIR__).'/.env'); + +if (empty($_ENV['OPENAI_API_KEY'])) { + echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new DallE(DallE::DALL_E_2); + +$response = $platform->request( + model: $llm, + input: 'A cartoon-style elephant with a long trunk and large ears.', + options: ['response_format' => 'url', 'n' => 2], +); + +foreach ($response->getContent() as $index => $image) { + echo 'Image '.$index.': '.$image->url.PHP_EOL; +} diff --git a/examples/image-generator-dall-e-3.php b/examples/image-generator-dall-e-3.php new file mode 100644 index 0000000..72fb68d --- /dev/null +++ b/examples/image-generator-dall-e-3.php @@ -0,0 +1,36 @@ +loadEnv(dirname(__DIR__).'/.env'); + +if (empty($_ENV['OPENAI_API_KEY'])) { + echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new DallE(DallE::DALL_E_3); + +$response = $platform->request( + model: $llm, + input: 'A cartoon-style elephant with a long trunk and large ears.', + options: ['response_format' => 'url', 'n' => 1], +); + +if ($response instanceof AsyncResponse) { + $response = $response->unwrap(); +} + +assert($response instanceof GeneratedImagesResponse); + +echo 'Revised Prompt: '.$response->revisedPrompt.PHP_EOL.PHP_EOL; + +foreach ($response->getContent() as $index => $image) { + echo 'Image '.$index.': '.$image->url.PHP_EOL; +} diff --git a/src/Bridge/OpenAI/DallE.php b/src/Bridge/OpenAI/DallE.php new file mode 100644 index 0000000..ed3f19b --- /dev/null +++ b/src/Bridge/OpenAI/DallE.php @@ -0,0 +1,34 @@ + $options The default options for the model usage */ + public function __construct( + private string $version = self::DALL_E_2, + private array $options = [ + 'response_format' => 'url', + 'n' => 1, + ], + ) { + } + + public function getVersion(): string + { + return $this->version; + } + + /** @return array */ + public function getOptions(): array + { + return $this->options; + } +} diff --git a/src/Bridge/OpenAI/DallE/Base64Image.php b/src/Bridge/OpenAI/DallE/Base64Image.php new file mode 100644 index 0000000..d1f8c90 --- /dev/null +++ b/src/Bridge/OpenAI/DallE/Base64Image.php @@ -0,0 +1,16 @@ + */ + private readonly array $images; + + public function __construct( + public ?string $revisedPrompt = null, // Only string on Dall-E 3 usage + Base64Image|UrlImage ...$images, + ) { + $this->images = \array_values($images); + } + + /** + * @return list + */ + public function getContent(): array + { + return $this->images; + } +} diff --git a/src/Bridge/OpenAI/DallE/ModelClient.php b/src/Bridge/OpenAI/DallE/ModelClient.php new file mode 100644 index 0000000..bf599af --- /dev/null +++ b/src/Bridge/OpenAI/DallE/ModelClient.php @@ -0,0 +1,66 @@ +httpClient->request('POST', 'https://api.openai.com/v1/images/generations', [ + 'auth_bearer' => $this->apiKey, + 'json' => \array_merge($options, [ + 'model' => $model->getVersion(), + 'prompt' => $input, + ]), + ]); + } + + public function convert(HttpResponse $response, array $options = []): LlmResponse + { + $response = $response->toArray(); + if (!isset($response['data'][0])) { + throw new \RuntimeException('No image generated.'); + } + + $images = []; + foreach ($response['data'] as $image) { + if ('url' === $options['response_format']) { + $images[] = new UrlImage($image['url']); + + continue; + } + + $images[] = new Base64Image($image['b64_json']); + } + + return new GeneratedImagesResponse($image['revised_prompt'] ?? null, ...$images); + } +} diff --git a/src/Bridge/OpenAI/DallE/UrlImage.php b/src/Bridge/OpenAI/DallE/UrlImage.php new file mode 100644 index 0000000..a9b2d8d --- /dev/null +++ b/src/Bridge/OpenAI/DallE/UrlImage.php @@ -0,0 +1,16 @@ +encodedImage); + } + + #[Test] + public function itThrowsExceptionWhenBase64ImageIsEmpty(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The base64 encoded image generated must be given.'); + + new Base64Image(''); + } +} diff --git a/tests/Bridge/OpenAI/DallE/GeneratedImagesResponseTest.php b/tests/Bridge/OpenAI/DallE/GeneratedImagesResponseTest.php new file mode 100644 index 0000000..71098c5 --- /dev/null +++ b/tests/Bridge/OpenAI/DallE/GeneratedImagesResponseTest.php @@ -0,0 +1,56 @@ +revisedPrompt); + self::assertCount(1, $generatedImagesResponse->getContent()); + self::assertSame($base64Image, $generatedImagesResponse->getContent()[0]); + } + + #[Test] + public function itCreatesGeneratedImagesResponseWithRevisedPrompt(): void + { + $base64Image = new Base64Image('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); + $generatedImagesResponse = new GeneratedImagesResponse('revised prompt', $base64Image); + + self::assertSame('revised prompt', $generatedImagesResponse->revisedPrompt); + self::assertCount(1, $generatedImagesResponse->getContent()); + self::assertSame($base64Image, $generatedImagesResponse->getContent()[0]); + } + + #[Test] + public function itIsCreatableWithMultipleImages(): void + { + $image1 = new UrlImage('https://example'); + $image2 = new UrlImage('https://example2'); + + $generatedImagesResponse = new GeneratedImagesResponse(null, $image1, $image2); + + self::assertCount(2, $generatedImagesResponse->getContent()); + self::assertSame($image1, $generatedImagesResponse->getContent()[0]); + self::assertSame($image2, $generatedImagesResponse->getContent()[1]); + } +} diff --git a/tests/Bridge/OpenAI/DallE/ModelClientTest.php b/tests/Bridge/OpenAI/DallE/ModelClientTest.php new file mode 100644 index 0000000..253c7e4 --- /dev/null +++ b/tests/Bridge/OpenAI/DallE/ModelClientTest.php @@ -0,0 +1,97 @@ +supports(new DallE(), 'foo')); + } + + #[Test] + public function itIsExecutingTheCorrectRequest(): void + { + $httpClient = $this->createMock(HttpClientInterface::class); + $httpClient->expects(self::once()) + ->method('request') + ->with('POST', 'https://api.openai.com/v1/images/generations', [ + 'auth_bearer' => 'sk-api-key', + 'json' => [ + 'model' => DallE::DALL_E_2, + 'prompt' => 'foo', + 'n' => 1, + 'response_format' => 'url', + ], + ]); + + $modelClient = new ModelClient($httpClient, 'sk-api-key'); + $modelClient->request(new DallE(), 'foo', ['n' => 1, 'response_format' => 'url']); + } + + #[Test] + public function itIsConvertingTheResponse(): void + { + $httpResponse = self::createStub(HttpResponse::class); + $httpResponse->method('toArray')->willReturn([ + 'data' => [ + ['url' => 'https://example.com/image.jpg'], + ], + ]); + + $modelClient = new ModelClient(self::createStub(HttpClientInterface::class), 'sk-api-key'); + $response = $modelClient->convert($httpResponse, ['response_format' => 'url']); + + self::assertCount(1, $response->getContent()); + self::assertInstanceOf(UrlImage::class, $response->getContent()[0]); + self::assertSame('https://example.com/image.jpg', $response->getContent()[0]->url); + } + + #[Test] + public function itIsConvertingTheResponseWithRevisedPrompt(): void + { + $emptyPixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + + $httpResponse = self::createStub(HttpResponse::class); + $httpResponse->method('toArray')->willReturn([ + 'data' => [ + ['b64_json' => $emptyPixel, 'revised_prompt' => 'revised prompt'], + ], + ]); + + $modelClient = new ModelClient(self::createStub(HttpClientInterface::class), 'sk-api-key'); + $response = $modelClient->convert($httpResponse, ['response_format' => 'b64_json']); + + self::assertInstanceOf(GeneratedImagesResponse::class, $response); + self::assertCount(1, $response->getContent()); + self::assertInstanceOf(Base64Image::class, $response->getContent()[0]); + self::assertSame($emptyPixel, $response->getContent()[0]->encodedImage); + self::assertSame('revised prompt', $response->revisedPrompt); + } +} diff --git a/tests/Bridge/OpenAI/DallE/UrlImageTest.php b/tests/Bridge/OpenAI/DallE/UrlImageTest.php new file mode 100644 index 0000000..31c2420 --- /dev/null +++ b/tests/Bridge/OpenAI/DallE/UrlImageTest.php @@ -0,0 +1,33 @@ +url); + } + + #[Test] + public function itThrowsExceptionWhenUrlIsEmpty(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The image url must be given.'); + + new UrlImage(''); + } +} diff --git a/tests/Bridge/OpenAI/DallETest.php b/tests/Bridge/OpenAI/DallETest.php new file mode 100644 index 0000000..2e31219 --- /dev/null +++ b/tests/Bridge/OpenAI/DallETest.php @@ -0,0 +1,34 @@ +getVersion()); + self::assertSame(['response_format' => 'url', 'n' => 1], $dallE->getOptions()); + } + + #[Test] + public function itCreatesDallEWithCustomSettings(): void + { + $dallE = new DallE(DallE::DALL_E_3, ['response_format' => 'base64', 'n' => 2]); + + self::assertSame(DallE::DALL_E_3, $dallE->getVersion()); + self::assertSame(['response_format' => 'base64', 'n' => 2], $dallE->getOptions()); + } +}