From ddaf5173d1739b6fcb846866ef77865a7c45a033 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Thu, 23 Oct 2025 15:57:10 +0200 Subject: [PATCH 1/3] feat(platform): Introduce Decart support for image / video --- examples/.env | 4 + examples/decart/README.md | 9 +++ examples/decart/text-to-image.php | 24 ++++++ examples/decart/text-to-video.php | 24 ++++++ .../Bridge/Decart/Contract/DecartContract.php | 21 ++++++ src/platform/src/Bridge/Decart/Decart.php | 21 ++++++ .../src/Bridge/Decart/DecartClient.php | 59 +++++++++++++++ .../Bridge/Decart/DecartResultConverter.php | 41 +++++++++++ .../src/Bridge/Decart/ModelCatalog.php | 73 +++++++++++++++++++ .../src/Bridge/Decart/PlatformFactory.php | 42 +++++++++++ src/platform/src/Capability.php | 9 +++ .../tests/Bridge/Decart/DecartClientTest.php | 32 ++++++++ 12 files changed, 359 insertions(+) create mode 100644 examples/decart/README.md create mode 100644 examples/decart/text-to-image.php create mode 100644 examples/decart/text-to-video.php create mode 100644 src/platform/src/Bridge/Decart/Contract/DecartContract.php create mode 100644 src/platform/src/Bridge/Decart/Decart.php create mode 100644 src/platform/src/Bridge/Decart/DecartClient.php create mode 100644 src/platform/src/Bridge/Decart/DecartResultConverter.php create mode 100644 src/platform/src/Bridge/Decart/ModelCatalog.php create mode 100644 src/platform/src/Bridge/Decart/PlatformFactory.php create mode 100644 src/platform/tests/Bridge/Decart/DecartClientTest.php diff --git a/examples/.env b/examples/.env index d12133301..18c2459c3 100644 --- a/examples/.env +++ b/examples/.env @@ -56,6 +56,10 @@ ELEVEN_LABS_API_KEY= CARTESIA_API_KEY= CARTESIA_API_VERSION=2025-04-16 +# For using Decart +DECART_ENDPOINT=https://api.decart.ai/v1 +DECART_API_KEY= + # For using SerpApi (tool) SERP_API_KEY= diff --git a/examples/decart/README.md b/examples/decart/README.md new file mode 100644 index 000000000..537c03ac7 --- /dev/null +++ b/examples/decart/README.md @@ -0,0 +1,9 @@ +# Decart Examples + +One use case of Decart is to convert text to image, which creates video files from text input. + +To run the examples, you can use additional tools like (mpg123)[https://www.mpg123.de/]: + +```bash +php elevenlabs/text-to-speech.php | mpg123 - +``` diff --git a/examples/decart/text-to-image.php b/examples/decart/text-to-image.php new file mode 100644 index 000000000..5743a905c --- /dev/null +++ b/examples/decart/text-to-image.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\Decart\PlatformFactory; +use Symfony\AI\Platform\Message\Content\Text; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create( + apiKey: env('DECART_API_KEY'), + httpClient: http_client() +); + +$result = $platform->invoke('lucy-pro-t2i', new Text('A cat on a kitchen table')); + +echo $result->asBinary().\PHP_EOL; diff --git a/examples/decart/text-to-video.php b/examples/decart/text-to-video.php new file mode 100644 index 000000000..6e35cc2e9 --- /dev/null +++ b/examples/decart/text-to-video.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\Decart\PlatformFactory; +use Symfony\AI\Platform\Message\Content\Text; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create( + apiKey: env('DECART_API_KEY'), + httpClient: http_client(), +); + +$result = $platform->invoke('lucy-pro-t2v', new Text('A serene ocean with dolphins jumping at sunset')); + +echo $result->asText(); diff --git a/src/platform/src/Bridge/Decart/Contract/DecartContract.php b/src/platform/src/Bridge/Decart/Contract/DecartContract.php new file mode 100644 index 000000000..a02ab215f --- /dev/null +++ b/src/platform/src/Bridge/Decart/Contract/DecartContract.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart\Contract; + +use Symfony\AI\Platform\Contract; + +/** + * @author Guillaume Loulier + */ +final readonly class DecartContract extends Contract +{ +} diff --git a/src/platform/src/Bridge/Decart/Decart.php b/src/platform/src/Bridge/Decart/Decart.php new file mode 100644 index 000000000..f5d5cdd17 --- /dev/null +++ b/src/platform/src/Bridge/Decart/Decart.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart; + +use Symfony\AI\Platform\Model; + +/** + * @author Guillaume Loulier + */ +final class Decart extends Model +{ +} diff --git a/src/platform/src/Bridge/Decart/DecartClient.php b/src/platform/src/Bridge/Decart/DecartClient.php new file mode 100644 index 000000000..9b692ecca --- /dev/null +++ b/src/platform/src/Bridge/Decart/DecartClient.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart; + +use Symfony\AI\Platform\Model; +use Symfony\AI\Platform\ModelClientInterface; +use Symfony\AI\Platform\Result\RawHttpResult; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Guillaume Loulier + */ +final readonly class DecartClient implements ModelClientInterface +{ + public function __construct( + private HttpClientInterface $httpClient, + #[\SensitiveParameter] private string $apiKey, + private ?string $hostUrl = 'https://api.decart.ai/v1', + ) { + } + + public function supports(Model $model): bool + { + return $model instanceof Decart; + } + + public function request(Model $model, array|string $payload, array $options = []): RawResultInterface + { + dd($this->httpClient->request('POST', \sprintf('%s/generate/%s', $this->hostUrl, $model->getName()), [ + 'headers' => [ + 'x-api-key' => $this->apiKey, + ], + 'body' => [ + 'prompt' => \is_string($payload) ? $payload : $payload['text'], + ...$options, + ], + ])->getContent(false)); + + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/generate/%s', $this->hostUrl, $model->getName()), [ + 'headers' => [ + 'x-api-key' => $this->apiKey, + ], + 'body' => [ + 'prompt' => \is_string($payload) ? $payload : $payload['text'], + ...$options, + ], + ])); + } +} diff --git a/src/platform/src/Bridge/Decart/DecartResultConverter.php b/src/platform/src/Bridge/Decart/DecartResultConverter.php new file mode 100644 index 000000000..ec620f4d5 --- /dev/null +++ b/src/platform/src/Bridge/Decart/DecartResultConverter.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart; + +use Symfony\AI\Platform\Exception\RuntimeException; +use Symfony\AI\Platform\Model; +use Symfony\AI\Platform\Result\BinaryResult; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\Result\ResultInterface; +use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Guillaume Loulier + */ +final readonly class DecartResultConverter implements ResultConverterInterface +{ + public function supports(Model $model): bool + { + return $model instanceof Decart; + } + + public function convert(RawResultInterface $result, array $options = []): ResultInterface + { + /** @var ResponseInterface $response */ + $response = $result->getObject(); + + $headers = $response->getHeaders(); + + return new BinaryResult($response->getContent(), $headers['content-type']); + } +} diff --git a/src/platform/src/Bridge/Decart/ModelCatalog.php b/src/platform/src/Bridge/Decart/ModelCatalog.php new file mode 100644 index 000000000..5ab8ffa4d --- /dev/null +++ b/src/platform/src/Bridge/Decart/ModelCatalog.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart; + +use Symfony\AI\Platform\Capability; +use Symfony\AI\Platform\ModelCatalog\AbstractModelCatalog; + +/** + * @author Guillaume Loulier + */ +final class ModelCatalog extends AbstractModelCatalog +{ + /** + * @param array}> $additionalModels + */ + public function __construct(array $additionalModels = []) + { + $defaultModels = [ + 'lucy-dev-i2v' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::IMAGE_TO_VIDEO, + Capability::VIDEO_TO_VIDEO, + ], + ], + 'lucy-pro-t2i' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::TEXT_TO_IMAGE, + ], + ], + 'lucy-pro-t2v' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::TEXT_TO_VIDEO, + Capability::IMAGE_TO_VIDEO, + ], + ], + 'lucy-pro-i2i' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::IMAGE_TO_IMAGE, + ], + ], + 'lucy-pro-v2v' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::VIDEO_TO_VIDEO, + ], + ], + 'lucy-pro-flf2v' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::IMAGE_TO_VIDEO, + ], + ], + ]; + + $this->models = [ + ...$defaultModels, + ...$additionalModels, + ]; + } +} diff --git a/src/platform/src/Bridge/Decart/PlatformFactory.php b/src/platform/src/Bridge/Decart/PlatformFactory.php new file mode 100644 index 000000000..17ce09b99 --- /dev/null +++ b/src/platform/src/Bridge/Decart/PlatformFactory.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart; + +use Symfony\AI\Platform\Bridge\Decart\Contract\DecartContract; +use Symfony\AI\Platform\Contract; +use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; +use Symfony\AI\Platform\Platform; +use Symfony\Component\HttpClient\EventSourceHttpClient; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Guillaume Loulier + */ +final readonly class PlatformFactory +{ + public static function create( + string $apiKey, + ?string $hostUrl = 'https://api.decart.ai/v1', + ?HttpClientInterface $httpClient = null, + ModelCatalogInterface $modelCatalog = new ModelCatalog(), + ?Contract $contract = null, + ): Platform { + $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); + + return new Platform( + [new DecartClient($httpClient, $apiKey, $hostUrl)], + [new DecartResultConverter()], + $modelCatalog, + $contract ?? DecartContract::create(), + ); + } +} diff --git a/src/platform/src/Capability.php b/src/platform/src/Capability.php index 47e6afc59..cf48a3b68 100644 --- a/src/platform/src/Capability.php +++ b/src/platform/src/Capability.php @@ -43,6 +43,15 @@ enum Capability: string case TEXT_TO_SPEECH = 'text-to-speech'; case SPEECH_TO_TEXT = 'speech-to-text'; + // IMAGE + case TEXT_TO_IMAGE = 'text-to-image'; + case IMAGE_TO_IMAGE = 'image-to-image'; + + // VIDEO + case TEXT_TO_VIDEO = 'text-to-video'; + case IMAGE_TO_VIDEO = 'image-to-video'; + case VIDEO_TO_VIDEO = 'video-to-video'; + // EMBEDDINGS case EMBEDDINGS = 'embeddings'; diff --git a/src/platform/tests/Bridge/Decart/DecartClientTest.php b/src/platform/tests/Bridge/Decart/DecartClientTest.php new file mode 100644 index 000000000..cdc2c5bfd --- /dev/null +++ b/src/platform/tests/Bridge/Decart/DecartClientTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\Decart; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Decart\Decart; +use Symfony\AI\Platform\Bridge\Decart\DecartClient; +use Symfony\AI\Platform\Model; +use Symfony\Component\HttpClient\MockHttpClient; + +final class DecartClientTest extends TestCase +{ + public function testSupportsModel() + { + $client = new DecartClient( + new MockHttpClient(), + 'my-api-key', + ); + + $this->assertTrue($client->supports(new Decart('lucy-dev-i2v'))); + $this->assertFalse($client->supports(new Model('any-model'))); + } +} From db06a3223af0ed0e29d11b8138720ec2cc2d46c1 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Wed, 26 Nov 2025 21:44:22 +0100 Subject: [PATCH 2/3] ref --- src/platform/src/Bridge/Decart/DecartClient.php | 2 +- src/platform/src/Bridge/Decart/PlatformFactory.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/src/Bridge/Decart/DecartClient.php b/src/platform/src/Bridge/Decart/DecartClient.php index 9b692ecca..9bd012259 100644 --- a/src/platform/src/Bridge/Decart/DecartClient.php +++ b/src/platform/src/Bridge/Decart/DecartClient.php @@ -25,7 +25,7 @@ public function __construct( private HttpClientInterface $httpClient, #[\SensitiveParameter] private string $apiKey, - private ?string $hostUrl = 'https://api.decart.ai/v1', + private string $hostUrl = 'https://api.decart.ai/v1', ) { } diff --git a/src/platform/src/Bridge/Decart/PlatformFactory.php b/src/platform/src/Bridge/Decart/PlatformFactory.php index 17ce09b99..d87b52bf0 100644 --- a/src/platform/src/Bridge/Decart/PlatformFactory.php +++ b/src/platform/src/Bridge/Decart/PlatformFactory.php @@ -15,6 +15,7 @@ use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; use Symfony\AI\Platform\Platform; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -29,6 +30,7 @@ public static function create( ?HttpClientInterface $httpClient = null, ModelCatalogInterface $modelCatalog = new ModelCatalog(), ?Contract $contract = null, + ?EventDispatcherInterface $eventDispatcher = null ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); @@ -37,6 +39,7 @@ public static function create( [new DecartResultConverter()], $modelCatalog, $contract ?? DecartContract::create(), + $eventDispatcher, ); } } From cec3afbcc6536b5ba7e6c2010cf526533c4c959d Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Fri, 28 Nov 2025 11:53:16 +0100 Subject: [PATCH 3/3] ref --- examples/decart/animated-image.php | 26 +++++++++ examples/decart/image-editing.php | 26 +++++++++ examples/decart/text-to-video.php | 6 +- .../Bridge/Decart/Contract/DecartContract.php | 11 +++- .../Decart/Contract/ImageNormalizer.php | 58 +++++++++++++++++++ .../Decart/Contract/VideoNormalizer.php | 49 ++++++++++++++++ .../src/Bridge/Decart/DecartClient.php | 24 +++++++- .../Bridge/Decart/DecartResultConverter.php | 3 +- .../src/Bridge/Decart/ModelCatalog.php | 6 ++ .../src/Bridge/Decart/PlatformFactory.php | 2 +- src/platform/src/Message/Content/Video.php | 19 ++++++ .../Decart/Contract/DecartContractTest.php | 25 ++++++++ .../tests/Bridge/Decart/ModelCatalogTest.php | 30 ++++++++++ 13 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 examples/decart/animated-image.php create mode 100644 examples/decart/image-editing.php create mode 100644 src/platform/src/Bridge/Decart/Contract/ImageNormalizer.php create mode 100644 src/platform/src/Bridge/Decart/Contract/VideoNormalizer.php create mode 100644 src/platform/src/Message/Content/Video.php create mode 100644 src/platform/tests/Bridge/Decart/Contract/DecartContractTest.php create mode 100644 src/platform/tests/Bridge/Decart/ModelCatalogTest.php diff --git a/examples/decart/animated-image.php b/examples/decart/animated-image.php new file mode 100644 index 000000000..43661bcd3 --- /dev/null +++ b/examples/decart/animated-image.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\Decart\PlatformFactory; +use Symfony\AI\Platform\Message\Content\Image; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create( + apiKey: env('DECART_API_KEY'), + httpClient: http_client(), +); + +$result = $platform->invoke('lucy-pro-i2v', Image::fromFile(dirname(__DIR__, 2).'/fixtures/accordion.jpg'), [ + 'prompt' => 'Make the man move from right to left while playing accordion', +]); + +echo $result->asBinary().\PHP_EOL; diff --git a/examples/decart/image-editing.php b/examples/decart/image-editing.php new file mode 100644 index 000000000..5002bd374 --- /dev/null +++ b/examples/decart/image-editing.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\Decart\PlatformFactory; +use Symfony\AI\Platform\Message\Content\Image; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create( + apiKey: env('DECART_API_KEY'), + httpClient: http_client() +); + +$result = $platform->invoke('lucy-pro-i2i', Image::fromFile(dirname(__DIR__, 2).'/fixtures/accordion.jpg'), [ + 'prompt' => 'Colorize the walls', +]); + +echo $result->asBinary().\PHP_EOL; diff --git a/examples/decart/text-to-video.php b/examples/decart/text-to-video.php index 6e35cc2e9..dd406d57a 100644 --- a/examples/decart/text-to-video.php +++ b/examples/decart/text-to-video.php @@ -19,6 +19,8 @@ httpClient: http_client(), ); -$result = $platform->invoke('lucy-pro-t2v', new Text('A serene ocean with dolphins jumping at sunset')); +$result = $platform->invoke('lucy-pro-t2v', new Text('A serene ocean with dolphins jumping at sunset'), [ + 'timeout' => 3600, +]); -echo $result->asText(); +echo $result->asBinary(); diff --git a/src/platform/src/Bridge/Decart/Contract/DecartContract.php b/src/platform/src/Bridge/Decart/Contract/DecartContract.php index a02ab215f..416d11a72 100644 --- a/src/platform/src/Bridge/Decart/Contract/DecartContract.php +++ b/src/platform/src/Bridge/Decart/Contract/DecartContract.php @@ -12,10 +12,19 @@ namespace Symfony\AI\Platform\Bridge\Decart\Contract; use Symfony\AI\Platform\Contract; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** * @author Guillaume Loulier */ -final readonly class DecartContract extends Contract +final class DecartContract extends Contract { + public static function create(NormalizerInterface ...$normalizer): Contract + { + return parent::create( + new ImageNormalizer(), + new VideoNormalizer(), + ...$normalizer + ); + } } diff --git a/src/platform/src/Bridge/Decart/Contract/ImageNormalizer.php b/src/platform/src/Bridge/Decart/Contract/ImageNormalizer.php new file mode 100644 index 000000000..d21333b9c --- /dev/null +++ b/src/platform/src/Bridge/Decart/Contract/ImageNormalizer.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart\Contract; + +use Symfony\AI\Platform\Message\Content\Image; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Guillaume Loulier + */ +final class ImageNormalizer implements NormalizerInterface +{ + /** + * @param Image $data + * + * @return array{type: 'input_image', input_image: array{ + * data: string, + * path: string, + * format: 'jpg'|'png'|string, + * }} + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + return [ + 'type' => 'input_image', + 'input_image' => [ + 'data' => $data->asBase64(), + 'path' => $data->asPath(), + 'format' => match ($data->getFormat()) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + default => $data->getFormat(), + }, + ], + ]; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Image; + } + + public function getSupportedTypes(?string $format): array + { + return [ + Image::class => true, + ]; + } +} diff --git a/src/platform/src/Bridge/Decart/Contract/VideoNormalizer.php b/src/platform/src/Bridge/Decart/Contract/VideoNormalizer.php new file mode 100644 index 000000000..00ce2e25b --- /dev/null +++ b/src/platform/src/Bridge/Decart/Contract/VideoNormalizer.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Decart\Contract; + +use Symfony\AI\Platform\Message\Content\Video; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Guillaume Loulier + */ +final class VideoNormalizer implements NormalizerInterface +{ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + return [ + 'type' => 'input_video', + 'input_video' => [ + 'data' => $data->asBase64(), + 'path' => $data->asPath(), + 'format' => match ($data->getFormat()) { + 'audio/mpeg' => 'mp3', + 'audio/wav' => 'wav', + default => $data->getFormat(), + }, + ], + ]; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Video; + } + + public function getSupportedTypes(?string $format): array + { + return [ + Video::class => true, + ]; + } +} diff --git a/src/platform/src/Bridge/Decart/DecartClient.php b/src/platform/src/Bridge/Decart/DecartClient.php index 9bd012259..8bd05fa5c 100644 --- a/src/platform/src/Bridge/Decart/DecartClient.php +++ b/src/platform/src/Bridge/Decart/DecartClient.php @@ -11,6 +11,8 @@ namespace Symfony\AI\Platform\Bridge\Decart; +use Symfony\AI\Platform\Capability; +use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -36,7 +38,19 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawResultInterface { - dd($this->httpClient->request('POST', \sprintf('%s/generate/%s', $this->hostUrl, $model->getName()), [ + return match (true) { + \in_array(Capability::TEXT_TO_IMAGE, $model->getCapabilities(), true), + \in_array(Capability::TEXT_TO_VIDEO, $model->getCapabilities(), true) => $this->generate($model, $payload, $options), + \in_array(Capability::IMAGE_TO_IMAGE, $model->getCapabilities(), true), + \in_array(Capability::IMAGE_TO_VIDEO, $model->getCapabilities(), true), + \in_array(Capability::VIDEO_TO_VIDEO, $model->getCapabilities(), true) => $this->edit($model, $payload, $options), + default => throw new InvalidArgumentException(\sprintf('The "%s" model is not supported', $model->getName())), + }; + } + + private function generate(Model $model, array|string $payload, array $options = []): RawResultInterface + { + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/generate/%s', $this->hostUrl, $model->getName()), [ 'headers' => [ 'x-api-key' => $this->apiKey, ], @@ -44,14 +58,18 @@ public function request(Model $model, array|string $payload, array $options = [] 'prompt' => \is_string($payload) ? $payload : $payload['text'], ...$options, ], - ])->getContent(false)); + ])); + } + private function edit(Model $model, array|string $payload, array $options): RawResultInterface + { return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/generate/%s', $this->hostUrl, $model->getName()), [ 'headers' => [ 'x-api-key' => $this->apiKey, ], 'body' => [ - 'prompt' => \is_string($payload) ? $payload : $payload['text'], + 'prompt' => $options['prompt'], + 'data' => fopen($payload['input_image']['path'] ?? $payload['input_video']['path'], 'r'), ...$options, ], ])); diff --git a/src/platform/src/Bridge/Decart/DecartResultConverter.php b/src/platform/src/Bridge/Decart/DecartResultConverter.php index ec620f4d5..2b3d380c9 100644 --- a/src/platform/src/Bridge/Decart/DecartResultConverter.php +++ b/src/platform/src/Bridge/Decart/DecartResultConverter.php @@ -11,7 +11,6 @@ namespace Symfony\AI\Platform\Bridge\Decart; -use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\BinaryResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -36,6 +35,6 @@ public function convert(RawResultInterface $result, array $options = []): Result $headers = $response->getHeaders(); - return new BinaryResult($response->getContent(), $headers['content-type']); + return new BinaryResult($response->getContent(), $headers['content-type'][0]); } } diff --git a/src/platform/src/Bridge/Decart/ModelCatalog.php b/src/platform/src/Bridge/Decart/ModelCatalog.php index 5ab8ffa4d..3292935a0 100644 --- a/src/platform/src/Bridge/Decart/ModelCatalog.php +++ b/src/platform/src/Bridge/Decart/ModelCatalog.php @@ -51,6 +51,12 @@ public function __construct(array $additionalModels = []) Capability::IMAGE_TO_IMAGE, ], ], + 'lucy-pro-i2v' => [ + 'class' => Decart::class, + 'capabilities' => [ + Capability::IMAGE_TO_VIDEO, + ], + ], 'lucy-pro-v2v' => [ 'class' => Decart::class, 'capabilities' => [ diff --git a/src/platform/src/Bridge/Decart/PlatformFactory.php b/src/platform/src/Bridge/Decart/PlatformFactory.php index d87b52bf0..700ec8c35 100644 --- a/src/platform/src/Bridge/Decart/PlatformFactory.php +++ b/src/platform/src/Bridge/Decart/PlatformFactory.php @@ -30,7 +30,7 @@ public static function create( ?HttpClientInterface $httpClient = null, ModelCatalogInterface $modelCatalog = new ModelCatalog(), ?Contract $contract = null, - ?EventDispatcherInterface $eventDispatcher = null + ?EventDispatcherInterface $eventDispatcher = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); diff --git a/src/platform/src/Message/Content/Video.php b/src/platform/src/Message/Content/Video.php new file mode 100644 index 000000000..087581526 --- /dev/null +++ b/src/platform/src/Message/Content/Video.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Message\Content; + +/** + * @author Guillaume Loulier + */ +final class Video extends File +{ +} diff --git a/src/platform/tests/Bridge/Decart/Contract/DecartContractTest.php b/src/platform/tests/Bridge/Decart/Contract/DecartContractTest.php new file mode 100644 index 000000000..4d3da0ba0 --- /dev/null +++ b/src/platform/tests/Bridge/Decart/Contract/DecartContractTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\Decart\Contract; + +use PHPUnit\Framework\TestCase; + +final class DecartContractTest extends TestCase +{ + public function testItCanCreatePayloadWithAudio() + { + } + + public function testItCanCreatePayloadWithVide() + { + } +} diff --git a/src/platform/tests/Bridge/Decart/ModelCatalogTest.php b/src/platform/tests/Bridge/Decart/ModelCatalogTest.php new file mode 100644 index 000000000..41250ef8b --- /dev/null +++ b/src/platform/tests/Bridge/Decart/ModelCatalogTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\Decart; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Decart\ModelCatalog; +use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; +use Symfony\AI\Platform\Test\ModelCatalogTestCase; + +final class ModelCatalogTest extends ModelCatalogTestCase +{ + public static function modelsProvider(): iterable + { + // TODO: Implement modelsProvider() method. + } + + protected function createModelCatalog(): ModelCatalogInterface + { + return new ModelCatalog(); + } +}