Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand Down
9 changes: 9 additions & 0 deletions examples/decart/README.md
Original file line number Diff line number Diff line change
@@ -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 -
```
26 changes: 26 additions & 0 deletions examples/decart/animated-image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
26 changes: 26 additions & 0 deletions examples/decart/image-editing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
24 changes: 24 additions & 0 deletions examples/decart/text-to-image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
26 changes: 26 additions & 0 deletions examples/decart/text-to-video.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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'), [
'timeout' => 3600,
]);

echo $result->asBinary();
30 changes: 30 additions & 0 deletions src/platform/src/Bridge/Decart/Contract/DecartContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class DecartContract extends Contract
{
public static function create(NormalizerInterface ...$normalizer): Contract
{
return parent::create(
new ImageNormalizer(),
new VideoNormalizer(),
...$normalizer
);
}
}
58 changes: 58 additions & 0 deletions src/platform/src/Bridge/Decart/Contract/ImageNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <personal@guillaumeloulier.fr>
*/
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,
];
}
}
49 changes: 49 additions & 0 deletions src/platform/src/Bridge/Decart/Contract/VideoNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <personal@guillaumeloulier.fr>
*/
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,
];
}
}
21 changes: 21 additions & 0 deletions src/platform/src/Bridge/Decart/Decart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <personal@guillaumeloulier.fr>
*/
final class Decart extends Model
{
}
77 changes: 77 additions & 0 deletions src/platform/src/Bridge/Decart/DecartClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Exception\InvalidArgumentException;
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 <personal@guillaumeloulier.fr>
*/
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
{
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,
],
'body' => [
'prompt' => \is_string($payload) ? $payload : $payload['text'],
...$options,
],
]));
}

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' => $options['prompt'],
'data' => fopen($payload['input_image']['path'] ?? $payload['input_video']['path'], 'r'),
...$options,
],
]));
}
}
Loading
Loading