From 27a81216734f400bae00523a0897739a380d0e03 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 7 Dec 2025 02:38:29 +0100 Subject: [PATCH] Extend gemini catalog for image preview and editing example --- examples/gemini/.gitignore | 1 + examples/gemini/image-editing.php | 31 +++++++++++++++++++ .../Bridge/Gemini/Gemini/ResultConverter.php | 5 +-- .../src/Bridge/Gemini/ModelCatalog.php | 18 +++++++++++ src/platform/src/Result/BinaryResult.php | 11 +++++++ .../Gemini/Gemini/ResultConverterTest.php | 16 ++++++---- 6 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 examples/gemini/.gitignore create mode 100644 examples/gemini/image-editing.php diff --git a/examples/gemini/.gitignore b/examples/gemini/.gitignore new file mode 100644 index 000000000..b43160cac --- /dev/null +++ b/examples/gemini/.gitignore @@ -0,0 +1 @@ +result.png diff --git a/examples/gemini/image-editing.php b/examples/gemini/image-editing.php new file mode 100644 index 000000000..d8758aafc --- /dev/null +++ b/examples/gemini/image-editing.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory; +use Symfony\AI\Platform\Message\Content\Image; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create(env('GEMINI_API_KEY'), http_client()); + +$messages = new MessageBag( + Message::ofUser( + 'Please colorize the elephant in red.', + Image::fromFile(dirname(__DIR__, 2).'/fixtures/image.jpg'), + ), +); +$result = $platform->invoke('gemini-2.5-flash-image', $messages); + +file_put_contents(__DIR__.'/result.png', $result->asBinary()); + +echo 'Result image saved to result.png'.\PHP_EOL; diff --git a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php index d69999378..54cb7b1f4 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php @@ -127,7 +127,7 @@ private function convertChoice(array $choice): ToolCallResult|TextResult|BinaryR } if (isset($contentPart['inlineData'])) { - return new BinaryResult($contentPart['inlineData']['data'], $contentPart['inlineData']['mimeType'] ?? null); + return BinaryResult::fromBase64($contentPart['inlineData']['data'], $contentPart['inlineData']['mimeType'] ?? null); } throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finishReason'])); @@ -150,7 +150,8 @@ private function convertChoice(array $choice): ToolCallResult|TextResult|BinaryR return new TextResult($content); } - throw new RuntimeException('Code execution failed.'); + // TODO: see https://github.com/symfony/ai/issues/1053 + throw new RuntimeException('Choice conversion failed. Potentially due to multiple content parts.'); } /** diff --git a/src/platform/src/Bridge/Gemini/ModelCatalog.php b/src/platform/src/Bridge/Gemini/ModelCatalog.php index 348860987..af9222ff2 100644 --- a/src/platform/src/Bridge/Gemini/ModelCatalog.php +++ b/src/platform/src/Bridge/Gemini/ModelCatalog.php @@ -37,6 +37,24 @@ public function __construct(array $additionalModels = []) Capability::TOOL_CALLING, ], ], + 'gemini-3-pro-image-preview' => [ + 'class' => Gemini::class, + 'capabilities' => [ + Capability::INPUT_MESSAGES, + Capability::INPUT_IMAGE, + Capability::OUTPUT_IMAGE, + Capability::OUTPUT_TEXT, + ], + ], + 'gemini-2.5-flash-image' => [ + 'class' => Gemini::class, + 'capabilities' => [ + Capability::INPUT_MESSAGES, + Capability::INPUT_IMAGE, + Capability::OUTPUT_IMAGE, + Capability::OUTPUT_TEXT, + ], + ], 'gemini-2.5-flash' => [ 'class' => Gemini::class, 'capabilities' => [ diff --git a/src/platform/src/Result/BinaryResult.php b/src/platform/src/Result/BinaryResult.php index 4f810f1b2..8e91dca26 100644 --- a/src/platform/src/Result/BinaryResult.php +++ b/src/platform/src/Result/BinaryResult.php @@ -24,6 +24,17 @@ public function __construct( ) { } + public static function fromBase64(string $base64Data, ?string $mimeType = null): self + { + $data = base64_decode($base64Data, true); + + if (false === $data) { + throw new RuntimeException('The provided data is not valid base64-encoded data.'); + } + + return new self($data, $mimeType); + } + public function getMimeType(): ?string { return $this->mimeType; diff --git a/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php b/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php index 846d115c9..9f21feaf1 100644 --- a/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php +++ b/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\Gemini\Gemini\ResultConverter; use Symfony\AI\Platform\Exception\RuntimeException; +use Symfony\AI\Platform\Message\Content\Image; use Symfony\AI\Platform\Result\BinaryResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\ToolCall; @@ -83,6 +84,7 @@ public function testConvertsInlineDataToBinaryResult() $converter = new ResultConverter(); $httpResponse = self::createMock(ResponseInterface::class); $httpResponse->method('getStatusCode')->willReturn(200); + $image = Image::fromFile(\dirname(__DIR__, 6).'/fixtures/image.jpg'); $httpResponse->method('toArray')->willReturn([ 'candidates' => [ [ @@ -90,8 +92,8 @@ public function testConvertsInlineDataToBinaryResult() 'parts' => [ [ 'inlineData' => [ - 'mimeType' => 'image/png', - 'data' => 'base64EncodedImageData', + 'mimeType' => 'image/jpeg', + 'data' => $image->asBase64(), ], ], ], @@ -102,8 +104,9 @@ public function testConvertsInlineDataToBinaryResult() $result = $converter->convert(new RawHttpResult($httpResponse)); $this->assertInstanceOf(BinaryResult::class, $result); - $this->assertSame('base64EncodedImageData', $result->getContent()); - $this->assertSame('image/png', $result->getMimeType()); + $this->assertSame($image->asBinary(), $result->getContent()); + $this->assertSame('image/jpeg', $result->getMimeType()); + $this->assertSame($image->asDataUrl(), $result->toDataUri()); } public function testConvertsInlineDataWithoutMimeTypeToBinaryResult() @@ -111,6 +114,7 @@ public function testConvertsInlineDataWithoutMimeTypeToBinaryResult() $converter = new ResultConverter(); $httpResponse = self::createMock(ResponseInterface::class); $httpResponse->method('getStatusCode')->willReturn(200); + $image = Image::fromFile(\dirname(__DIR__, 6).'/fixtures/image.jpg'); $httpResponse->method('toArray')->willReturn([ 'candidates' => [ [ @@ -118,7 +122,7 @@ public function testConvertsInlineDataWithoutMimeTypeToBinaryResult() 'parts' => [ [ 'inlineData' => [ - 'data' => 'base64EncodedData', + 'data' => $image->asBase64(), ], ], ], @@ -129,7 +133,7 @@ public function testConvertsInlineDataWithoutMimeTypeToBinaryResult() $result = $converter->convert(new RawHttpResult($httpResponse)); $this->assertInstanceOf(BinaryResult::class, $result); - $this->assertSame('base64EncodedData', $result->getContent()); + $this->assertSame($image->asBinary(), $result->getContent()); $this->assertNull($result->getMimeType()); } }