Skip to content

Commit 08a825d

Browse files
committed
tests
1 parent c6e92f6 commit 08a825d

File tree

8 files changed

+280
-33
lines changed

8 files changed

+280
-33
lines changed

src/ai-bundle/config/options.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
->end()
4343
->arrayNode('eleven_labs')
4444
->children()
45+
->scalarNode('host')->end()
4546
->scalarNode('api_key')->isRequired()->end()
4647
->scalarNode('output_path')->isRequired()->end()
47-
->scalarNode('host')->end()
4848
->end()
4949
->end()
5050
->arrayNode('gemini')

src/ai-bundle/doc/index.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Configuration
3434
class: 'Symfony\AI\Platform\Bridge\OpenAi\Gpt'
3535
name: !php/const Symfony\AI\Platform\Bridge\OpenAi\Gpt::GPT_4O_MINI
3636
37-
**Advanced Example with Anthropic, Azure, Gemini and multiple agents**
37+
**Advanced Example with Anthropic, Azure, ElevenLabs, Gemini, Ollamaand multiple agents**
3838

3939
.. code-block:: yaml
4040
@@ -50,6 +50,10 @@ Configuration
5050
deployment: '%env(AZURE_OPENAI_GPT)%'
5151
api_key: '%env(AZURE_OPENAI_KEY)%'
5252
api_version: '%env(AZURE_GPT_VERSION)%'
53+
eleven_labs:
54+
host: '%env(ELEVEN_LABS_HOST)%'
55+
api_key: '%env(ELEVEN_LABS_API_KEY)%'
56+
output_path: '%env(ELEVEN_LABS_OUTPUT_PATH)%'
5357
gemini:
5458
api_key: '%env(GEMINI_API_KEY)%'
5559
ollama:
@@ -85,6 +89,12 @@ Configuration
8589
tools: # If undefined, all tools are injected into the agent, use "tools: false" to disable tools.
8690
- 'Symfony\AI\Agent\Toolbox\Tool\Wikipedia'
8791
fault_tolerant_toolbox: false # Disables fault tolerant toolbox, default is true
92+
audio:
93+
platform: 'ai.platform.eleven_labs'
94+
model:
95+
class: 'Symfony\AI\Platform\Bridge\ElevenLabs'
96+
name: !php/const Symfony\AI\Platform\Bridge\ElevenLabs\TEXT_TO_SPEECH
97+
tools: false
8898
store:
8999
# also azure_search, meilisearch, memory, mongodb, pinecone, qdrant and surrealdb are supported as store type
90100
chroma_db:

src/ai-bundle/src/AiBundle.php

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,21 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
210210
}
211211

212212
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');
213+
$platformId = 'ai.platform.eleven_labs';
214+
$definition = (new Definition(Platform::class))
215+
->setFactory(ElevenLabsPlatformFactory::class.'::create')
216+
->setLazy(true)
217+
->addTag('proxy', ['interface' => PlatformInterface::class])
218+
->setArguments([
219+
$platform['host'],
220+
$platform['api_key'],
221+
$platform['output_path'],
222+
new Reference('http_client', ContainerInterface::NULL_ON_INVALID_REFERENCE),
223+
new Reference('ai.platform.contract.default'),
224+
])
225+
->addTag('ai.platform');
227226

228-
$container->setDefinition($platformId, $definition);
229-
}
227+
$container->setDefinition($platformId, $definition);
230228

231229
return;
232230
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ private function getFullConfig(): array
144144
],
145145
],
146146
'eleven_labs' => [
147+
'host' => 'https://api.elevenlabs.io/v1',
147148
'api_key' => 'eleven_labs_key_full',
148149
'output_path' => 'path/to/output',
149150
],

src/platform/src/Bridge/ElevenLabs/ElevenLabsClient.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public function request(Model $model, array|string $payload, array $options = []
4040
return match ($model->getName()) {
4141
ElevenLabs::SPEECH_TO_TEXT => $this->doSpeechToTextRequest($model, $payload, $options),
4242
ElevenLabs::TEXT_TO_SPEECH => $this->doTextToSpeechRequest($model, $payload, $options),
43-
default => throw new InvalidArgumentException(\sprintf('Unsupported model "%s".', $model->getName())),
4443
};
4544
}
4645

@@ -51,7 +50,7 @@ private function doSpeechToTextRequest(Model $model, array|string $payload, arra
5150
}
5251

5352
if (!is_array($payload)) {
54-
throw new InvalidArgumentException('The payload must be an array, received %s.', get_debug_type($payload));
53+
throw new InvalidArgumentException(sprintf('The payload must be an array, received "%s".', get_debug_type($payload)));
5554
}
5655

5756
$model = $options['model'] ??= $model->getOptions()['model'];
@@ -73,8 +72,12 @@ private function doTextToSpeechRequest(Model $model, array|string $payload, arra
7372
throw new InvalidArgumentException('The model option is required.');
7473
}
7574

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));
75+
if (!is_array($payload)) {
76+
throw new InvalidArgumentException(sprintf('The payload must be an array, received "%s".', get_debug_type($payload)));
77+
}
78+
79+
if (!array_key_exists('text', $payload)) {
80+
throw new InvalidArgumentException('The payload must contain a "text" key.');
7881
}
7982

8083
$model = $options['model'] ??= $model->getOptions()['model'];

src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,15 @@ public function convert(RawResultInterface $result, array $options = []): Result
5050
$response = $result->getObject();
5151

5252
return match (true) {
53-
str_contains($response->getInfo('url'), 'speech-to-text', ) => $this->doConvertSpeechToText($response),
54-
str_contains($response->getInfo('url'), 'text-to-speech') => $this->doConvertTextToSpeech($response),
53+
str_contains($response->getInfo('url'), 'speech-to-text') => new TextResult($result->getData()['text']),
54+
str_contains($response->getInfo('url'), 'text-to-speech') => $this->doConvertTextToSpeech($result),
5555
default => throw new RuntimeException('Unsupported ElevenLabs response.'),
5656
};
5757
}
5858

59-
private function doConvertSpeechToText(ResponseInterface $response): ResultInterface
59+
private function doConvertTextToSpeech(RawResultInterface $result): ResultInterface
6060
{
61-
$payload = $response->toArray();
62-
63-
return new TextResult($payload['text']);
64-
}
65-
66-
private function doConvertTextToSpeech(ResponseInterface $response): ResultInterface
67-
{
68-
$payload = $response->getContent();
61+
$payload = $result->getObject()->getContent();
6962

7063
$path = sprintf('%s/%s.mp3', $this->outputPath, uniqid());
7164

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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\Tests\Bridge\ElevenLabs;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\UsesClass;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabs;
18+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsClient;
19+
use Symfony\AI\Platform\Contract\Normalizer\Message\Content\AudioNormalizer;
20+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
21+
use Symfony\AI\Platform\Message\Content\Audio;
22+
use Symfony\AI\Platform\Model;
23+
use Symfony\Component\HttpClient\MockHttpClient;
24+
25+
#[CoversClass(ElevenLabsClient::class)]
26+
#[UsesClass(ElevenLabs::class)]
27+
#[UsesClass(Model::class)]
28+
#[UsesClass(Audio::class)]
29+
#[UsesClass(AudioNormalizer::class)]
30+
final class ElevenLabsClientTest extends TestCase
31+
{
32+
public function testSupportsModel()
33+
{
34+
$client = new ElevenLabsClient(
35+
new MockHttpClient(),
36+
'https://api.elevenlabs.io/v1',
37+
'my-api-key',
38+
);
39+
40+
$this->assertTrue($client->supports(new ElevenLabs()));
41+
$this->assertFalse($client->supports(new Model('any-model')));
42+
}
43+
44+
public function testClientCannotPerformSpeechToTextRequestWithoutModel()
45+
{
46+
$normalizer = new AudioNormalizer();
47+
48+
$client = new ElevenLabsClient(
49+
new MockHttpClient(),
50+
'https://api.elevenlabs.io/v1',
51+
'my-api-key',
52+
);
53+
54+
$payload = $normalizer->normalize(Audio::fromFile(dirname(__DIR__, 5).'/fixtures/audio.mp3'));
55+
56+
$this->expectException(InvalidArgumentException::class);
57+
$this->expectExceptionMessage('The model option is required.');
58+
$this->expectExceptionCode(0);
59+
$client->request(new ElevenLabs(ElevenLabs::SPEECH_TO_TEXT), $payload);
60+
}
61+
62+
public function testClientCannotPerformSpeechToTextRequestWithInvalidPayload()
63+
{
64+
$client = new ElevenLabsClient(
65+
new MockHttpClient(),
66+
'https://api.elevenlabs.io/v1',
67+
'my-api-key',
68+
);
69+
70+
$this->expectException(InvalidArgumentException::class);
71+
$this->expectExceptionMessage('The payload must be an array, received "string".');
72+
$this->expectExceptionCode(0);
73+
$client->request(new ElevenLabs(ElevenLabs::SPEECH_TO_TEXT, options: [
74+
'model' => 'bar',
75+
]), 'foo');
76+
}
77+
78+
public function testClientCanPerformSpeechToTextRequest()
79+
{
80+
$httpClient = new MockHttpClient();
81+
$normalizer = new AudioNormalizer();
82+
83+
$client = new ElevenLabsClient(
84+
$httpClient,
85+
'https://api.elevenlabs.io/v1',
86+
'my-api-key',
87+
);
88+
89+
$payload = $normalizer->normalize(Audio::fromFile(dirname(__DIR__, 5).'/fixtures/audio.mp3'));
90+
91+
$client->request(new ElevenLabs(ElevenLabs::SPEECH_TO_TEXT, options: [
92+
'model' => 'bar',
93+
]), $payload);
94+
95+
$this->assertSame(1, $httpClient->getRequestsCount());
96+
}
97+
98+
public function testClientCannotPerformTextToSpeechRequestWithoutModel()
99+
{
100+
$client = new ElevenLabsClient(
101+
new MockHttpClient(),
102+
'https://api.elevenlabs.io/v1',
103+
'my-api-key',
104+
);
105+
106+
$this->expectException(InvalidArgumentException::class);
107+
$this->expectExceptionMessage('The model option is required.');
108+
$this->expectExceptionCode(0);
109+
$client->request(new ElevenLabs(), []);
110+
}
111+
112+
public function testClientCannotPerformTextToSpeechRequestWithInvalidPayload()
113+
{
114+
$client = new ElevenLabsClient(
115+
new MockHttpClient(),
116+
'https://api.elevenlabs.io/v1',
117+
'my-api-key',
118+
);
119+
120+
$this->expectException(InvalidArgumentException::class);
121+
$this->expectExceptionMessage('The payload must be an array, received "string".');
122+
$this->expectExceptionCode(0);
123+
$client->request(new ElevenLabs(options: [
124+
'model' => 'bar',
125+
]), 'foo');
126+
}
127+
128+
public function testClientCannotPerformTextToSpeechRequestWithoutValidPayload()
129+
{
130+
$normalizer = new AudioNormalizer();
131+
132+
$client = new ElevenLabsClient(
133+
new MockHttpClient(),
134+
'https://api.elevenlabs.io/v1',
135+
'my-api-key',
136+
);
137+
138+
$this->expectException(InvalidArgumentException::class);
139+
$this->expectExceptionMessage('The payload must contain a "text" key');
140+
$this->expectExceptionCode(0);
141+
$client->request(new ElevenLabs(options: [
142+
'model' => 'bar',
143+
]), []);
144+
}
145+
146+
public function testClientCanPerformTextToSpeechRequest()
147+
{
148+
$httpClient = new MockHttpClient();
149+
150+
$client = new ElevenLabsClient(
151+
$httpClient,
152+
'https://api.elevenlabs.io/v1',
153+
'my-api-key',
154+
);
155+
156+
$client->request(new ElevenLabs(options: [
157+
'model' => 'bar',
158+
]), [
159+
'text' => 'foo',
160+
]);
161+
162+
$this->assertSame(1, $httpClient->getRequestsCount());
163+
}
164+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Tests\Bridge\ElevenLabs;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\UsesClass;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabs;
18+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsResultConverter;
19+
use Symfony\AI\Platform\Model;
20+
use Symfony\AI\Platform\Result\AudioResult;
21+
use Symfony\AI\Platform\Result\InMemoryRawResult;
22+
use Symfony\AI\Platform\Result\TextResult;
23+
24+
#[CoversClass(ElevenLabsResultConverter::class)]
25+
#[UsesClass(ElevenLabs::class)]
26+
#[UsesClass(Model::class)]
27+
#[UsesClass(TextResult::class)]
28+
#[UsesClass(AudioResult::class)]
29+
#[UsesClass(InMemoryRawResult::class)]
30+
final class ElevenLabsConverterTest extends TestCase
31+
{
32+
public function testSupportsModel()
33+
{
34+
$converter = new ElevenLabsResultConverter(sys_get_temp_dir());
35+
36+
$this->assertTrue($converter->supports(new ElevenLabs()));
37+
$this->assertFalse($converter->supports(new Model('any-model')));
38+
}
39+
40+
public function testConvertSpeechToTextResponse()
41+
{
42+
$converter = new ElevenLabsResultConverter(sys_get_temp_dir());
43+
$rawResult = new InMemoryRawResult([
44+
'text' => 'Hello there',
45+
], new class() {
46+
public function getInfo(): string
47+
{
48+
return 'speech-to-text';
49+
}
50+
});
51+
52+
$result = $converter->convert($rawResult);
53+
54+
$this->assertInstanceOf(TextResult::class, $result);
55+
$this->assertSame('Hello there', $result->getContent());
56+
}
57+
58+
public function testConvertTextToSpeechResponse()
59+
{
60+
$converter = new ElevenLabsResultConverter(sys_get_temp_dir());
61+
$rawResult = new InMemoryRawResult([], new class() {
62+
public function getInfo(): string
63+
{
64+
return 'text-to-speech';
65+
}
66+
67+
public function getContent()
68+
{
69+
return file_get_contents(dirname(__DIR__, 5).'/fixtures/audio.mp3');
70+
}
71+
});
72+
73+
$result = $converter->convert($rawResult);
74+
75+
$this->assertInstanceOf(AudioResult::class, $result);
76+
$this->assertSame('audio/mpeg', $result->getMimeType());
77+
}
78+
}

0 commit comments

Comments
 (0)