Skip to content

Commit c6e92f6

Browse files
committed
feat(platform): ElevenLabs platform
1 parent a50d26e commit c6e92f6

File tree

16 files changed

+389
-2
lines changed

16 files changed

+389
-2
lines changed

examples/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ HUGGINGFACE_KEY=
4343
# For using OpenRouter
4444
OPENROUTER_KEY=
4545

46+
# For using ElevenLabs
47+
ELEVEN_LABS_URL=https://api.elevenlabs.io/v1
48+
ELEVEN_LABS_API_KEY=
49+
4650
# For using SerpApi (tool)
4751
SERP_API_KEY=
4852

examples/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"symfony/event-dispatcher": "^6.4|^7.0",
2323
"symfony/filesystem": "^6.4|^7.0",
2424
"symfony/finder": "^6.4|^7.0",
25+
"symfony/mime": "^7.3",
2526
"symfony/process": "^6.4|^7.0",
2627
"symfony/var-dumper": "^6.4|^7.0"
2728
},
@@ -33,6 +34,7 @@
3334
"config": {
3435
"allow-plugins": {
3536
"codewithkyrian/platform-package-installer": true,
37+
"codewithkyrian/transformers-libsloader": true,
3638
"php-http/discovery": true
3739
},
3840
"sort-packages": true
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabs;
13+
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
14+
use Symfony\AI\Platform\Message\Content\Audio;
15+
16+
require_once dirname(__DIR__).'/bootstrap.php';
17+
18+
$platform = PlatformFactory::create(
19+
env('ELEVEN_LABS_URL'),
20+
env('ELEVEN_LABS_API_KEY'),
21+
__DIR__.'/tmp',
22+
http_client()
23+
);
24+
$model = new ElevenLabs(ElevenLabs::SPEECH_TO_TEXT, options: [
25+
'model' => 'scribe_v1',
26+
]);
27+
$file = Audio::fromFile(dirname(__DIR__, 2).'/fixtures/audio.mp3');
28+
29+
$result = $platform->invoke($model, $file);
30+
31+
echo $result->asText().\PHP_EOL;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabs;
13+
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
14+
use Symfony\AI\Platform\Message\Content\Text;
15+
16+
require_once dirname(__DIR__).'/bootstrap.php';
17+
18+
$platform = PlatformFactory::create(
19+
env('ELEVEN_LABS_URL'),
20+
env('ELEVEN_LABS_API_KEY'),
21+
__DIR__.'/tmp',
22+
http_client(),
23+
);
24+
$model = new ElevenLabs(options: [
25+
'model' => 'Dslrhjl3ZpzrctukrQSN', # Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
26+
]);
27+
28+
$result = $platform->invoke($model, new Text('Hello world'));
29+
30+
echo $result->asAudio().\PHP_EOL;

src/ai-bundle/config/options.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
->end()
4141
->end()
4242
->end()
43+
->arrayNode('eleven_labs')
44+
->children()
45+
->scalarNode('api_key')->isRequired()->end()
46+
->scalarNode('output_path')->isRequired()->end()
47+
->scalarNode('host')->end()
48+
->end()
49+
->end()
4350
->arrayNode('gemini')
4451
->children()
4552
->scalarNode('api_key')->isRequired()->end()

src/ai-bundle/src/AiBundle.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Symfony\AI\AiBundle\Security\Attribute\IsGrantedTool;
2828
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
2929
use Symfony\AI\Platform\Bridge\Azure\OpenAi\PlatformFactory as AzureOpenAiPlatformFactory;
30+
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
3031
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory as GeminiPlatformFactory;
3132
use Symfony\AI\Platform\Bridge\LmStudio\PlatformFactory as LmStudioPlatformFactory;
3233
use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory as MistralPlatformFactory;
@@ -208,6 +209,28 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
208209
return;
209210
}
210211

212+
if ('eleven_labs' === $type) {
213+
foreach ($platform as $name => $config) {
214+
$platformId = 'ai.platform.eleven_labs.'.$name;
215+
$definition = (new Definition(Platform::class))
216+
->setFactory(ElevenLabsPlatformFactory::class.'::create')
217+
->setLazy(true)
218+
->addTag('proxy', ['interface' => PlatformInterface::class])
219+
->setArguments([
220+
$config['host'],
221+
$config['api_key'],
222+
$config['output_path'],
223+
new Reference('http_client', ContainerInterface::NULL_ON_INVALID_REFERENCE),
224+
new Reference('ai.platform.contract.eleven_labs'),
225+
])
226+
->addTag('ai.platform');
227+
228+
$container->setDefinition($platformId, $definition);
229+
}
230+
231+
return;
232+
}
233+
211234
if ('gemini' === $type) {
212235
$platformId = 'ai.platform.gemini';
213236
$definition = (new Definition(Platform::class))

src/ai-bundle/tests/DependencyInjection/AiBundleTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ private function getFullConfig(): array
143143
'api_version' => '2024-02-15-preview',
144144
],
145145
],
146+
'eleven_labs' => [
147+
'api_key' => 'eleven_labs_key_full',
148+
'output_path' => 'path/to/output',
149+
],
146150
'gemini' => [
147151
'api_key' => 'gemini_key_full',
148152
],

src/platform/composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@
4040
"phpstan/phpstan": "^2.1.17",
4141
"phpstan/phpstan-symfony": "^2.0.6",
4242
"phpunit/phpunit": "^11.5",
43+
"symfony/ai-agent": "@dev",
4344
"symfony/console": "^6.4 || ^7.1",
4445
"symfony/dotenv": "^6.4 || ^7.1",
45-
"symfony/ai-agent": "@dev",
4646
"symfony/event-dispatcher": "^6.4 || ^7.1",
47+
"symfony/filesystem": "^7.3",
4748
"symfony/finder": "^6.4 || ^7.1",
49+
"symfony/mime": "^7.3",
4850
"symfony/process": "^6.4 || ^7.1",
4951
"symfony/var-dumper": "^6.4 || ^7.1"
5052
},
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\ElevenLabs;
13+
14+
use Symfony\AI\Platform\Model;
15+
16+
/**
17+
* @author Guillaume Loulier <[email protected]>
18+
*/
19+
final class ElevenLabs extends Model
20+
{
21+
public const TEXT_TO_SPEECH = 'text-to-speech';
22+
public const SPEECH_TO_TEXT = 'speech-to-text';
23+
24+
public function __construct(
25+
string $name = self::TEXT_TO_SPEECH,
26+
array $capabilities = [],
27+
array $options = [],
28+
) {
29+
parent::__construct($name, $capabilities, $options);
30+
}
31+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\ElevenLabs;
13+
14+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
15+
use Symfony\AI\Platform\Model;
16+
use Symfony\AI\Platform\ModelClientInterface;
17+
use Symfony\AI\Platform\Result\RawHttpResult;
18+
use Symfony\AI\Platform\Result\RawResultInterface;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
21+
/**
22+
* @author Guillaume Loulier <[email protected]>
23+
*/
24+
final readonly class ElevenLabsClient implements ModelClientInterface
25+
{
26+
public function __construct(
27+
private HttpClientInterface $httpClient,
28+
private string $hostUrl,
29+
private string $apiKey,
30+
) {
31+
}
32+
33+
public function supports(Model $model): bool
34+
{
35+
return $model instanceof ElevenLabs;
36+
}
37+
38+
public function request(Model $model, array|string $payload, array $options = []): RawResultInterface
39+
{
40+
return match ($model->getName()) {
41+
ElevenLabs::SPEECH_TO_TEXT => $this->doSpeechToTextRequest($model, $payload, $options),
42+
ElevenLabs::TEXT_TO_SPEECH => $this->doTextToSpeechRequest($model, $payload, $options),
43+
default => throw new InvalidArgumentException(\sprintf('Unsupported model "%s".', $model->getName())),
44+
};
45+
}
46+
47+
private function doSpeechToTextRequest(Model $model, array|string $payload, array $options): RawHttpResult
48+
{
49+
if (!array_key_exists('model', $model->getOptions())) {
50+
throw new InvalidArgumentException('The model option is required.');
51+
}
52+
53+
if (!is_array($payload)) {
54+
throw new InvalidArgumentException('The payload must be an array, received %s.', get_debug_type($payload));
55+
}
56+
57+
$model = $options['model'] ??= $model->getOptions()['model'];
58+
59+
return new RawHttpResult($this->httpClient->request('POST', sprintf('%s/speech-to-text', $this->hostUrl), [
60+
'headers' => [
61+
'xi-api-key' => $this->apiKey,
62+
],
63+
'body' => [
64+
'file' => $payload['input_audio']['resource'],
65+
'model_id' => $model,
66+
],
67+
]));
68+
}
69+
70+
private function doTextToSpeechRequest(Model $model, array|string $payload, array $options): RawHttpResult
71+
{
72+
if (!array_key_exists('model', $model->getOptions())) {
73+
throw new InvalidArgumentException('The model option is required.');
74+
}
75+
76+
if (is_array($payload) && !array_key_exists('text', $payload)) {
77+
throw new InvalidArgumentException('The payload must be an array, received %s.', get_debug_type($payload));
78+
}
79+
80+
$model = $options['model'] ??= $model->getOptions()['model'];
81+
82+
return new RawHttpResult($this->httpClient->request('POST', sprintf('%s/text-to-speech/%s', $this->hostUrl, $model), [
83+
'headers' => [
84+
'xi-api-key' => $this->apiKey,
85+
],
86+
'json' => [
87+
'text' => $payload['text'],
88+
'model_id' => 'eleven_multilingual_v2',
89+
],
90+
]));
91+
}
92+
}

0 commit comments

Comments
 (0)