Skip to content
Merged
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 docs/components/platform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ Supported Models & Platforms
* `LM Studio Catalog`_ and `HuggingFace`_ Models with `LM Studio`_ as Platform.
* All models provided by `HuggingFace`_ can be listed with a command in the examples folder,
and also filtered, e.g. ``php examples/huggingface/_model-listing.php --provider=hf-inference --task=object-detection``
* **Voice Models**
* `Cartesia TTS` with `Cartesia`_ as Platform
* `Cartesia STT` with `Cartesia`_ as Platform

Options
-------
Expand Down Expand Up @@ -463,6 +466,7 @@ Code Examples
.. _`Anthropic's Claude`: https://www.anthropic.com/claude
.. _`Anthropic`: https://www.anthropic.com/
.. _`AWS Bedrock`: https://aws.amazon.com/bedrock/
.. _`Cartesia`: https://cartesia.ai/sonic
.. _`Meta's Llama`: https://www.llama.com/
.. _`Ollama`: https://ollama.com/
.. _`Replicate`: https://replicate.com/
Expand Down
4 changes: 4 additions & 0 deletions examples/.env
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ OPENROUTER_KEY=
ELEVEN_LABS_URL=https://api.elevenlabs.io/v1
ELEVEN_LABS_API_KEY=

# For using Cartesia
CARTESIA_API_KEY=
CARTESIA_API_VERSION=2025-04-16

# For using SerpApi (tool)
SERP_API_KEY=

Expand Down
9 changes: 9 additions & 0 deletions examples/cartesia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Cartesia Examples

One use case of Cartesia is to convert text to speech, which creates audio files from text input.

To run the examples, you can use additional tools like (mpg123)[https://www.mpg123.de/]:

```bash
php cartesia/text-to-speech.php | mpg123 -
```
25 changes: 25 additions & 0 deletions examples/cartesia/speech-to-text.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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\Cartesia\PlatformFactory;
use Symfony\AI\Platform\Message\Content\Audio;

require_once dirname(__DIR__).'/bootstrap.php';

$platform = PlatformFactory::create(
apiKey: env('CARTESIA_API_KEY'),
version: env('CARTESIA_API_VERSION'),
httpClient: http_client(),
);

$result = $platform->invoke('ink-whisper', Audio::fromFile(dirname(__DIR__, 2).'/fixtures/audio.mp3'));

echo $result->asText().\PHP_EOL;
32 changes: 32 additions & 0 deletions examples/cartesia/text-to-speech.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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\Cartesia\PlatformFactory;
use Symfony\AI\Platform\Message\Content\Text;

require_once dirname(__DIR__).'/bootstrap.php';

$platform = PlatformFactory::create(
apiKey: env('CARTESIA_API_KEY'),
version: env('CARTESIA_API_VERSION'),
httpClient: http_client(),
);

$result = $platform->invoke('sonic-3', new Text('Hello world'), [
'voice' => '6ccbfb76-1fc6-48f7-b71d-91ac6298247b', // Tessa (https://play.cartesia.ai/voices/6ccbfb76-1fc6-48f7-b71d-91ac6298247b)
'output_format' => [
'container' => 'mp3',
'sample_rate' => 48000,
'bit_rate' => 192000,
],
]);

echo $result->asBinary().\PHP_EOL;
10 changes: 10 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@
->end()
->end()
->end()
->arrayNode('cartesia')
->children()
->stringNode('api_key')->isRequired()->end()
->stringNode('version')->isRequired()->end()
->stringNode('http_client')
->defaultValue('http_client')
->info('Service ID of the HTTP client to use')
->end()
->end()
->end()
->arrayNode('eleven_labs')
->children()
->stringNode('host')->end()
Expand Down
2 changes: 2 additions & 0 deletions src/ai-bundle/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract;
use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog;
use Symfony\AI\Platform\Bridge\Anthropic\TokenOutputProcessor as AnthropicTokenOutputProcessor;
use Symfony\AI\Platform\Bridge\Cartesia\ModelCatalog as CartesiaModelCatalog;
use Symfony\AI\Platform\Bridge\Cerebras\ModelCatalog as CerebrasModelCatalog;
use Symfony\AI\Platform\Bridge\DeepSeek\ModelCatalog as DeepSeekModelCatalog;
use Symfony\AI\Platform\Bridge\DockerModelRunner\ModelCatalog as DockerModelRunnerModelCatalog;
Expand Down Expand Up @@ -85,6 +86,7 @@
// model catalog
->set('ai.platform.model_catalog.aimlapi', AiMlApiModelCatalog::class)
->set('ai.platform.model_catalog.anthropic', AnthropicModelCatalog::class)
->set('ai.platform.model_catalog.cartesia', CartesiaModelCatalog::class)
->set('ai.platform.model_catalog.cerebras', CerebrasModelCatalog::class)
->set('ai.platform.model_catalog.deepseek', DeepSeekModelCatalog::class)
->set('ai.platform.model_catalog.dockermodelrunner', DockerModelRunnerModelCatalog::class)
Expand Down
21 changes: 21 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Symfony\AI\Chat\MessageStoreInterface;
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
use Symfony\AI\Platform\Bridge\Cartesia\PlatformFactory as CartesiaPlatformFactory;
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory as DeepSeekPlatformFactory;
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory as DockerModelRunnerPlatformFactory;
Expand Down Expand Up @@ -293,6 +294,26 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
return;
}

if ('cartesia' === $type) {
$definition = (new Definition(Platform::class))
->setFactory(CartesiaPlatformFactory::class.'::create')
->setLazy(true)
->addTag('proxy', ['interface' => PlatformInterface::class])
->setArguments([
$platform['api_key'],
$platform['version'],
new Reference($platform['http_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE),
new Reference('ai.platform.model_catalog.cartesia'),
null,
new Reference('event_dispatcher'),
])
->addTag('ai.platform', ['name' => 'cartesia']);

$container->setDefinition('ai.platform.cartesia', $definition);

return;
}

if ('eleven_labs' === $type) {
$platformId = 'ai.platform.eleven_labs';
$definition = (new Definition(Platform::class))
Expand Down
5 changes: 5 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2795,6 +2795,11 @@ private function getFullConfig(): array
'api_version' => '2024-02-15-preview',
],
],
'cartesia' => [
'api_key' => 'cartesia_key_full',
'version' => '2025-04-16',
'http_client' => 'http_client',
],
'eleven_labs' => [
'host' => 'https://api.elevenlabs.io/v1',
'api_key' => 'eleven_labs_key_full',
Expand Down
1 change: 1 addition & 0 deletions src/platform/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ CHANGELOG
- AI/ML API (language models and embeddings)
- Docker Model Runner (local model hosting)
- Scaleway (language models like OpenAI OSS, Llama 4, Qwen 3, and more)
- Cartesia (voice model that supports both text-to-speech and speech-to-text)
* Add comprehensive message system with role-based messaging:
- `UserMessage` for user inputs with multi-modal content
- `SystemMessage` for system instructions
Expand Down
21 changes: 21 additions & 0 deletions src/platform/src/Bridge/Cartesia/Cartesia.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\Cartesia;

use Symfony\AI\Platform\Model;

/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class Cartesia extends Model
{
}
91 changes: 91 additions & 0 deletions src/platform/src/Bridge/Cartesia/CartesiaClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?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\Cartesia;

use Symfony\AI\Platform\Capability;
use Symfony\AI\Platform\Exception\RuntimeException;
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 class CartesiaClient implements ModelClientInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient,
#[\SensitiveParameter] private readonly string $apiKey,
private readonly string $version,
) {
}

public function supports(Model $model): bool
{
return $model instanceof Cartesia;
}

public function request(Model $model, array|string $payload, array $options = []): RawResultInterface
{
return match (true) {
\in_array(Capability::TEXT_TO_SPEECH, $model->getCapabilities()) => $this->doTextToSpeech($model, $payload, $options),
\in_array(Capability::SPEECH_TO_TEXT, $model->getCapabilities()) => $this->doSpeechToText($model, $payload, $options),
default => throw new RuntimeException(\sprintf('The model "%s" is not supported.', $model->getName())),
};
}

/**
* @param array<string|int, mixed> $payload
* @param array<string, mixed> $options
*/
private function doTextToSpeech(Model $model, array|string $payload, array $options): RawHttpResult
{
return new RawHttpResult($this->httpClient->request('POST', 'https://api.cartesia.ai/tts/bytes', [
'auth_bearer' => $this->apiKey,
'headers' => [
'Cartesia-Version' => $this->version,
],
'json' => [
...$options,
'model_id' => $model->getName(),
'transcript' => $payload['text'],
'voice' => [
'mode' => 'id',
'id' => $options['voice'],
],
'output_format' => $options['output_format'],
],
]));
}

/**
* @param array<string|int, mixed> $payload
* @param array<string, mixed> $options
*/
private function doSpeechToText(Model $model, array|string $payload, array $options): RawHttpResult
{
return new RawHttpResult($this->httpClient->request('POST', 'https://api.cartesia.ai/stt', [
'auth_bearer' => $this->apiKey,
'headers' => [
'Cartesia-Version' => $this->version,
],
'body' => [
...$options,
'model' => $model->getName(),
'file' => fopen($payload['input_audio']['path'], 'r'),
'timestamp_granularities[]' => 'word',
],
]));
}
}
44 changes: 44 additions & 0 deletions src/platform/src/Bridge/Cartesia/CartesiaResultConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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\Cartesia;

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\Result\TextResult;
use Symfony\AI\Platform\ResultConverterInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Guillaume Loulier <personal@guillaumeloulier.fr>
*/
final class CartesiaResultConverter implements ResultConverterInterface
{
public function supports(Model $model): bool
{
return $model instanceof Cartesia;
}

public function convert(RawResultInterface $result, array $options = []): ResultInterface
{
/** @var ResponseInterface $response */
$response = $result->getObject();

return match (true) {
str_contains($response->getInfo('url'), 'stt') => new TextResult($result->getData()['text']),
str_contains($response->getInfo('url'), 'tts') => new BinaryResult($result->getObject()->getContent(), 'audio/mpeg'),
default => throw new RuntimeException('Unsupported Cartesia response.'),
};
}
}
Loading