mirror of
https://github.com/symfony/ai.git
synced 2026-03-23 23:42:18 +01:00
feature #1615 [Platform][ElevenLabs] Improve support for ScopingHttpClient (Guikingone)
This PR was merged into the main branch.
Discussion
----------
[Platform][ElevenLabs] Improve support for `ScopingHttpClient`
| Q | A
| ------------- | ---
| Bug fix? | yes
| New feature? | no
| Docs? | no
l BC BREAK | Yes
| Issues | --
| License | MIT
* Improve support for `ScopingHttpClient` in `ElevenLabs` bridge
* Improve stream support for TTS
* Improve `match` usage
Commits
-------
a741f682 refactor(platform): ElevenLabs improvement
This commit is contained in:
10
UPGRADE.md
10
UPGRADE.md
@@ -1,3 +1,13 @@
|
||||
UPGRADE FROM 0.4 to 0.5
|
||||
=======================
|
||||
|
||||
Platform
|
||||
--------
|
||||
|
||||
* The `hostUrl` parameter for `ElevenLabsClient` has been removed
|
||||
* The `host` parameter for `ElevenLabsApiCatalog` has been removed
|
||||
* The `hostUrl` parameter for `PlatformFactory::create()` in `ElevenLabs` has been renamed to `endpoint`
|
||||
|
||||
UPGRADE FROM 0.3 to 0.4
|
||||
=======================
|
||||
|
||||
|
||||
@@ -14,10 +14,7 @@ use Symfony\AI\Platform\Message\Content\Audio;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(
|
||||
apiKey: env('ELEVEN_LABS_API_KEY'),
|
||||
httpClient: http_client()
|
||||
);
|
||||
$platform = PlatformFactory::create(env('ELEVEN_LABS_API_KEY'), httpClient: http_client());
|
||||
|
||||
$result = $platform->invoke('scribe_v1', Audio::fromFile(dirname(__DIR__, 2).'/fixtures/audio.mp3'));
|
||||
|
||||
|
||||
@@ -14,10 +14,7 @@ use Symfony\AI\Platform\Message\Content\Text;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(
|
||||
apiKey: env('ELEVEN_LABS_API_KEY'),
|
||||
httpClient: http_client(),
|
||||
);
|
||||
$platform = PlatformFactory::create(env('ELEVEN_LABS_API_KEY'), httpClient: http_client());
|
||||
|
||||
$result = $platform->invoke('eleven_multilingual_v2', new Text('The first move is what sets everything in motion.'), [
|
||||
'voice' => 'Dslrhjl3ZpzrctukrQSN', // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
|
||||
|
||||
@@ -14,10 +14,7 @@ use Symfony\AI\Platform\Message\Content\Text;
|
||||
|
||||
require_once dirname(__DIR__).'/bootstrap.php';
|
||||
|
||||
$platform = PlatformFactory::create(
|
||||
apiKey: env('ELEVEN_LABS_API_KEY'),
|
||||
httpClient: http_client(),
|
||||
);
|
||||
$platform = PlatformFactory::create(env('ELEVEN_LABS_API_KEY'), httpClient: http_client());
|
||||
|
||||
$result = $platform->invoke('eleven_multilingual_v2', new Text('Hello world'), [
|
||||
'voice' => 'Dslrhjl3ZpzrctukrQSN', // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.5
|
||||
---
|
||||
|
||||
* [BC BREAK] The `host_url` configuration key for `ElevenLabs` has been renamed `endpoint`
|
||||
|
||||
0.4
|
||||
---
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
return (new ArrayNodeDefinition('elevenlabs'))
|
||||
->children()
|
||||
->stringNode('api_key')->isRequired()->end()
|
||||
->stringNode('host')
|
||||
->defaultValue('https://api.elevenlabs.io/v1')
|
||||
->stringNode('endpoint')
|
||||
->defaultValue('https://api.elevenlabs.io/v1/')
|
||||
->end()
|
||||
->stringNode('http_client')
|
||||
->defaultValue('http_client')
|
||||
|
||||
@@ -36,6 +36,7 @@ use Symfony\AI\Platform\Bridge\Cerebras\ModelCatalog as CerebrasModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\Decart\ModelCatalog as DecartModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\DeepSeek\ModelCatalog as DeepSeekModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\DockerModelRunner\ModelCatalog as DockerModelRunnerModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\ElevenLabs\Contract\ElevenLabsContract;
|
||||
use Symfony\AI\Platform\Bridge\ElevenLabs\ModelCatalog as ElevenLabsModelCatalog;
|
||||
use Symfony\AI\Platform\Bridge\Gemini\Contract\GeminiContract;
|
||||
use Symfony\AI\Platform\Bridge\Gemini\ModelCatalog as GeminiModelCatalog;
|
||||
@@ -83,6 +84,8 @@ return static function (ContainerConfigurator $container): void {
|
||||
->factory([OpenAiContract::class, 'create'])
|
||||
->set('ai.platform.contract.anthropic', Contract::class)
|
||||
->factory([AnthropicContract::class, 'create'])
|
||||
->set('ai.platform.contract.elevenlabs', Contract::class)
|
||||
->factory([ElevenLabsContract::class, 'create'])
|
||||
->set('ai.platform.contract.gemini', Contract::class)
|
||||
->factory([GeminiContract::class, 'create'])
|
||||
->set('ai.platform.contract.huggingface', Contract::class)
|
||||
@@ -107,7 +110,7 @@ return static function (ContainerConfigurator $container): void {
|
||||
->set('ai.platform.model_catalog.deepseek', DeepSeekModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.dockermodelrunner', DockerModelRunnerModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.elevenlabs', ElevenLabsModelCatalog::class)
|
||||
->lazy(true)
|
||||
->lazy()
|
||||
->tag('proxy', ['interface' => ModelCatalogInterface::class])
|
||||
->set('ai.platform.model_catalog.gemini', GeminiModelCatalog::class)
|
||||
->set('ai.platform.model_catalog.huggingface', HuggingFaceModelCatalog::class)
|
||||
|
||||
@@ -605,13 +605,29 @@ final class AiBundle extends AbstractBundle
|
||||
throw new RuntimeException('ElevenLabs platform configuration requires "symfony/ai-eleven-labs-platform" package. Try running "composer require symfony/ai-eleven-labs-platform".');
|
||||
}
|
||||
|
||||
$httpClientReference = new Reference($platform['http_client']);
|
||||
|
||||
$scopedHttpClientDefinition = (new Definition(ScopingHttpClient::class))
|
||||
->setFactory([ScopingHttpClient::class, 'forBaseUri'])
|
||||
->setArguments([
|
||||
$httpClientReference,
|
||||
$platform['endpoint'],
|
||||
[
|
||||
'headers' => [
|
||||
'x-api-key' => $platform['api_key'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$container->setDefinition('ai.platform.elevenlabs.scoped_http_client', $scopedHttpClientDefinition);
|
||||
|
||||
$httpClientReference = new Reference('ai.platform.elevenlabs.scoped_http_client');
|
||||
|
||||
if (\array_key_exists('api_catalog', $platform) && $platform['api_catalog']) {
|
||||
$catalogDefinition = (new Definition(ElevenLabsApiCatalog::class))
|
||||
->setLazy(true)
|
||||
->setArguments([
|
||||
new Reference($platform['http_client']),
|
||||
$platform['api_key'],
|
||||
$platform['host'],
|
||||
$httpClientReference,
|
||||
])
|
||||
->addTag('proxy', ['interface' => ModelCatalogInterface::class]);
|
||||
|
||||
@@ -623,10 +639,10 @@ final class AiBundle extends AbstractBundle
|
||||
->setLazy(true)
|
||||
->setArguments([
|
||||
$platform['api_key'],
|
||||
$platform['host'],
|
||||
new Reference($platform['http_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE),
|
||||
$platform['endpoint'],
|
||||
$httpClientReference,
|
||||
new Reference('ai.platform.model_catalog.'.$type),
|
||||
null,
|
||||
new Reference('ai.platform.contract.'.$type),
|
||||
new Reference('event_dispatcher'),
|
||||
])
|
||||
->addTag('proxy', ['interface' => PlatformInterface::class])
|
||||
|
||||
@@ -3895,7 +3895,7 @@ class AiBundleTest extends TestCase
|
||||
$this->assertTrue($foundOutput, 'Default tool processor should have output tag with full agent ID');
|
||||
}
|
||||
|
||||
public function testElevenLabsPlatformCanBeRegistered()
|
||||
public function testElevenLabsPlatformCanBeConfigured()
|
||||
{
|
||||
$container = $this->buildContainer([
|
||||
'ai' => [
|
||||
@@ -3910,18 +3910,18 @@ class AiBundleTest extends TestCase
|
||||
$this->assertTrue($container->hasDefinition('ai.platform.elevenlabs'));
|
||||
|
||||
$definition = $container->getDefinition('ai.platform.elevenlabs');
|
||||
|
||||
$this->assertTrue($definition->isLazy());
|
||||
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
|
||||
|
||||
$this->assertCount(6, $definition->getArguments());
|
||||
$this->assertSame('foo', $definition->getArgument(0));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1', $definition->getArgument(1));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1/', $definition->getArgument(1));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
|
||||
$this->assertSame('http_client', (string) $definition->getArgument(2));
|
||||
$this->assertSame('ai.platform.elevenlabs.scoped_http_client', (string) $definition->getArgument(2));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
|
||||
$this->assertSame('ai.platform.model_catalog.elevenlabs', (string) $definition->getArgument(3));
|
||||
$this->assertNull($definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(4));
|
||||
$this->assertSame('ai.platform.contract.elevenlabs', (string) $definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(5));
|
||||
$this->assertSame('event_dispatcher', (string) $definition->getArgument(5));
|
||||
|
||||
@@ -3940,16 +3940,13 @@ class AiBundleTest extends TestCase
|
||||
|
||||
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
|
||||
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
|
||||
}
|
||||
|
||||
public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered()
|
||||
{
|
||||
$container = $this->buildContainer([
|
||||
'ai' => [
|
||||
'platform' => [
|
||||
'elevenlabs' => [
|
||||
'endpoint' => 'https://api.elevenlabs.io/v2',
|
||||
'api_key' => 'foo',
|
||||
'host' => 'https://api.elevenlabs.io/v2',
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -3958,7 +3955,6 @@ class AiBundleTest extends TestCase
|
||||
$this->assertTrue($container->hasDefinition('ai.platform.elevenlabs'));
|
||||
|
||||
$definition = $container->getDefinition('ai.platform.elevenlabs');
|
||||
|
||||
$this->assertTrue($definition->isLazy());
|
||||
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
|
||||
|
||||
@@ -3966,10 +3962,11 @@ class AiBundleTest extends TestCase
|
||||
$this->assertSame('foo', $definition->getArgument(0));
|
||||
$this->assertSame('https://api.elevenlabs.io/v2', $definition->getArgument(1));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
|
||||
$this->assertSame('http_client', (string) $definition->getArgument(2));
|
||||
$this->assertSame('ai.platform.elevenlabs.scoped_http_client', (string) $definition->getArgument(2));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
|
||||
$this->assertSame('ai.platform.model_catalog.elevenlabs', (string) $definition->getArgument(3));
|
||||
$this->assertNull($definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(4));
|
||||
$this->assertSame('ai.platform.contract.elevenlabs', (string) $definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(5));
|
||||
$this->assertSame('event_dispatcher', (string) $definition->getArgument(5));
|
||||
|
||||
@@ -3988,10 +3985,7 @@ class AiBundleTest extends TestCase
|
||||
|
||||
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
|
||||
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
|
||||
}
|
||||
|
||||
public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered()
|
||||
{
|
||||
$container = $this->buildContainer([
|
||||
'ai' => [
|
||||
'platform' => [
|
||||
@@ -4012,12 +4006,13 @@ class AiBundleTest extends TestCase
|
||||
|
||||
$this->assertCount(6, $definition->getArguments());
|
||||
$this->assertSame('foo', $definition->getArgument(0));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1', $definition->getArgument(1));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1/', $definition->getArgument(1));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
|
||||
$this->assertSame('foo', (string) $definition->getArgument(2));
|
||||
$this->assertSame('ai.platform.elevenlabs.scoped_http_client', (string) $definition->getArgument(2));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
|
||||
$this->assertSame('ai.platform.model_catalog.elevenlabs', (string) $definition->getArgument(3));
|
||||
$this->assertNull($definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(4));
|
||||
$this->assertSame('ai.platform.contract.elevenlabs', (string) $definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(5));
|
||||
$this->assertSame('event_dispatcher', (string) $definition->getArgument(5));
|
||||
|
||||
@@ -4036,10 +4031,7 @@ class AiBundleTest extends TestCase
|
||||
|
||||
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
|
||||
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
|
||||
}
|
||||
|
||||
public function testElevenLabsPlatformWithApiCatalogCanBeRegistered()
|
||||
{
|
||||
$container = $this->buildContainer([
|
||||
'ai' => [
|
||||
'platform' => [
|
||||
@@ -4061,12 +4053,13 @@ class AiBundleTest extends TestCase
|
||||
|
||||
$this->assertCount(6, $definition->getArguments());
|
||||
$this->assertSame('foo', $definition->getArgument(0));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1', $definition->getArgument(1));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1/', $definition->getArgument(1));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
|
||||
$this->assertSame('http_client', (string) $definition->getArgument(2));
|
||||
$this->assertSame('ai.platform.elevenlabs.scoped_http_client', (string) $definition->getArgument(2));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
|
||||
$this->assertSame('ai.platform.model_catalog.elevenlabs', (string) $definition->getArgument(3));
|
||||
$this->assertNull($definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(4));
|
||||
$this->assertSame('ai.platform.contract.elevenlabs', (string) $definition->getArgument(4));
|
||||
$this->assertInstanceOf(Reference::class, $definition->getArgument(5));
|
||||
$this->assertSame('event_dispatcher', (string) $definition->getArgument(5));
|
||||
|
||||
@@ -4082,11 +4075,9 @@ class AiBundleTest extends TestCase
|
||||
|
||||
$this->assertSame(ElevenLabsApiCatalog::class, $modelCatalogDefinition->getClass());
|
||||
$this->assertTrue($modelCatalogDefinition->isLazy());
|
||||
$this->assertCount(3, $modelCatalogDefinition->getArguments());
|
||||
$this->assertCount(1, $modelCatalogDefinition->getArguments());
|
||||
$this->assertInstanceOf(Reference::class, $modelCatalogDefinition->getArgument(0));
|
||||
$this->assertSame('http_client', (string) $modelCatalogDefinition->getArgument(0));
|
||||
$this->assertSame('foo', $modelCatalogDefinition->getArgument(1));
|
||||
$this->assertSame('https://api.elevenlabs.io/v1', $modelCatalogDefinition->getArgument(2));
|
||||
$this->assertSame('ai.platform.elevenlabs.scoped_http_client', (string) $modelCatalogDefinition->getArgument(0));
|
||||
|
||||
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
|
||||
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
|
||||
@@ -7602,7 +7593,7 @@ class AiBundleTest extends TestCase
|
||||
'api_key' => 'foo',
|
||||
],
|
||||
'elevenlabs' => [
|
||||
'host' => 'https://api.elevenlabs.io/v1',
|
||||
'endpoint' => 'https://api.elevenlabs.io/v1',
|
||||
'api_key' => 'elevenlabs_key_full',
|
||||
],
|
||||
'failover' => [
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.5
|
||||
---
|
||||
|
||||
* [BC BREAK] The `hostUrl` parameter for `ElevenLabsClient` has been removed
|
||||
* [BC BREAK] The `host` parameter for `ElevenLabsApiCatalog` has been removed
|
||||
* [BC BREAK] The `hostUrl` parameter for `PlatformFactory::create()` has been renamed to `endpoint`
|
||||
|
||||
0.3
|
||||
---
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ final class ElevenLabsApiCatalog implements ModelCatalogInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
#[\SensitiveParameter] private readonly string $apiKey,
|
||||
private readonly string $hostUrl = 'https://api.elevenlabs.io/v1',
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -45,11 +43,7 @@ final class ElevenLabsApiCatalog implements ModelCatalogInterface
|
||||
|
||||
public function getModels(): array
|
||||
{
|
||||
$response = $this->httpClient->request('GET', \sprintf('%s/models', $this->hostUrl), [
|
||||
'headers' => [
|
||||
'xi-api-key' => $this->apiKey,
|
||||
],
|
||||
]);
|
||||
$response = $this->httpClient->request('GET', '/models');
|
||||
|
||||
$models = $response->toArray();
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@ final class ElevenLabsClient implements ModelClientInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
#[\SensitiveParameter] private readonly string $apiKey,
|
||||
private readonly string $hostUrl = 'https://api.elevenlabs.io/v1',
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -42,18 +40,14 @@ final class ElevenLabsClient implements ModelClientInterface
|
||||
throw new InvalidArgumentException(\sprintf('The payload must be an array, received "%s".', get_debug_type($payload)));
|
||||
}
|
||||
|
||||
if ($model->supports(Capability::SPEECH_TO_TEXT)) {
|
||||
return $this->doSpeechToTextRequest($model, $payload);
|
||||
}
|
||||
|
||||
if ($model->supports(Capability::TEXT_TO_SPEECH)) {
|
||||
return $this->doTextToSpeechRequest($model, $payload, [
|
||||
return match (true) {
|
||||
$model->supports(Capability::SPEECH_TO_TEXT) => $this->doSpeechToTextRequest($model, $payload),
|
||||
$model->supports(Capability::TEXT_TO_SPEECH) => $this->doTextToSpeechRequest($model, $payload, [
|
||||
...$options,
|
||||
...$model->getOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(\sprintf('The model "%s" does not support text-to-speech or speech-to-text, please check the model information.', $model->getName()));
|
||||
]),
|
||||
default => throw new InvalidArgumentException(\sprintf('The model "%s" does not support text-to-speech or speech-to-text, please check the model information.', $model->getName())),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,10 +55,7 @@ final class ElevenLabsClient implements ModelClientInterface
|
||||
*/
|
||||
private function doSpeechToTextRequest(Model $model, array|string $payload): RawHttpResult
|
||||
{
|
||||
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/speech-to-text', $this->hostUrl), [
|
||||
'headers' => [
|
||||
'xi-api-key' => $this->apiKey,
|
||||
],
|
||||
return new RawHttpResult($this->httpClient->request('POST', 'speech-to-text', [
|
||||
'body' => [
|
||||
'file' => fopen($payload['input_audio']['path'], 'r'),
|
||||
'model_id' => $model->getName(),
|
||||
@@ -90,15 +81,12 @@ final class ElevenLabsClient implements ModelClientInterface
|
||||
$stream = $options['stream'] ?? false;
|
||||
|
||||
$url = $stream
|
||||
? \sprintf('%s/text-to-speech/%s/stream', $this->hostUrl, $voice)
|
||||
: \sprintf('%s/text-to-speech/%s', $this->hostUrl, $voice);
|
||||
? \sprintf('text-to-speech/%s/stream', $voice)
|
||||
: \sprintf('text-to-speech/%s', $voice);
|
||||
|
||||
unset($options['voice'], $options['stream']);
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', $url, [
|
||||
'headers' => [
|
||||
'xi-api-key' => $this->apiKey,
|
||||
],
|
||||
'json' => [
|
||||
'text' => $payload['text'],
|
||||
'model_id' => $model->getName(),
|
||||
|
||||
@@ -43,9 +43,9 @@ final class ElevenLabsResultConverter implements ResultConverterInterface
|
||||
$response = $result->getObject();
|
||||
|
||||
return match (true) {
|
||||
\array_key_exists('stream', $options) && $options['stream'] => new StreamResult($this->convertToGenerator($response)),
|
||||
str_contains($response->getInfo('url'), 'speech-to-text') => new TextResult($result->getData()['text']),
|
||||
str_contains($response->getInfo('url'), 'text-to-speech') && \array_key_exists('stream', $options) && $options['stream'] => new StreamResult($this->convertToGenerator($response)),
|
||||
str_contains($response->getInfo('url'), 'text-to-speech') => new BinaryResult($result->getObject()->getContent(), 'audio/mpeg'),
|
||||
str_contains($response->getInfo('url'), 'speech-to-text') => new TextResult($result->getData()['text']),
|
||||
default => throw new RuntimeException('Unsupported ElevenLabs response.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\AI\Platform\Contract;
|
||||
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
|
||||
use Symfony\AI\Platform\Platform;
|
||||
use Symfony\Component\HttpClient\EventSourceHttpClient;
|
||||
use Symfony\Component\HttpClient\ScopingHttpClient;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
@@ -25,8 +26,8 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
final class PlatformFactory
|
||||
{
|
||||
public static function create(
|
||||
string $apiKey,
|
||||
string $hostUrl = 'https://api.elevenlabs.io/v1',
|
||||
#[\SensitiveParameter] string $apiKey,
|
||||
string $endpoint = 'https://api.elevenlabs.io/v1/',
|
||||
?HttpClientInterface $httpClient = null,
|
||||
ModelCatalogInterface $modelCatalog = new ModelCatalog(),
|
||||
?Contract $contract = null,
|
||||
@@ -34,8 +35,14 @@ final class PlatformFactory
|
||||
): Platform {
|
||||
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
|
||||
|
||||
$httpClient = ScopingHttpClient::forBaseUri($httpClient, $endpoint, [
|
||||
'headers' => [
|
||||
'xi-api-key' => $apiKey,
|
||||
],
|
||||
]);
|
||||
|
||||
return new Platform(
|
||||
[new ElevenLabsClient($httpClient, $apiKey, $hostUrl)],
|
||||
[new ElevenLabsClient($httpClient)],
|
||||
[new ElevenLabsResultConverter($httpClient)],
|
||||
$modelCatalog,
|
||||
$contract ?? ElevenLabsContract::create(),
|
||||
|
||||
@@ -3,6 +3,14 @@ ElevenLabs Platform
|
||||
|
||||
ElevenLabs platform bridge for Symfony AI.
|
||||
|
||||
|
||||
Test Fixtures
|
||||
-------------
|
||||
|
||||
The test fixtures in `Tests/Fixtures/` contain binary media content with the following owners and licenses:
|
||||
|
||||
* `audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/)
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ final class ElevenLabsApiCatalogTest extends TestCase
|
||||
new JsonMockResponse([]),
|
||||
]);
|
||||
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo');
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The model "foo" cannot be retrieved from the API.');
|
||||
@@ -48,7 +48,7 @@ final class ElevenLabsApiCatalogTest extends TestCase
|
||||
]),
|
||||
]);
|
||||
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo');
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The model "foo" is not supported, please check the ElevenLabs API.');
|
||||
@@ -75,7 +75,7 @@ final class ElevenLabsApiCatalogTest extends TestCase
|
||||
]),
|
||||
]);
|
||||
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo');
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient);
|
||||
|
||||
$model = $modelCatalog->getModel('foo');
|
||||
|
||||
@@ -102,7 +102,7 @@ final class ElevenLabsApiCatalogTest extends TestCase
|
||||
]),
|
||||
]);
|
||||
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo');
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient);
|
||||
|
||||
$model = $modelCatalog->getModel('foo');
|
||||
|
||||
@@ -135,7 +135,7 @@ final class ElevenLabsApiCatalogTest extends TestCase
|
||||
]),
|
||||
]);
|
||||
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo');
|
||||
$modelCatalog = new ElevenLabsApiCatalog($httpClient);
|
||||
|
||||
$models = $modelCatalog->getModels();
|
||||
|
||||
|
||||
@@ -28,10 +28,7 @@ final class ElevenLabsClientTest extends TestCase
|
||||
{
|
||||
public function testSupportsModel()
|
||||
{
|
||||
$client = new ElevenLabsClient(
|
||||
new MockHttpClient(),
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient(new MockHttpClient());
|
||||
|
||||
$this->assertTrue($client->supports(new ElevenLabs('eleven_multilingual_v2')));
|
||||
$this->assertFalse($client->supports(new Model('any-model')));
|
||||
@@ -51,10 +48,7 @@ final class ElevenLabsClientTest extends TestCase
|
||||
]);
|
||||
$normalizer = new AudioNormalizer();
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$mockHttpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($mockHttpClient);
|
||||
|
||||
$payload = $normalizer->normalize(Audio::fromFile(\dirname(__DIR__, 6).'/fixtures/audio.mp3'));
|
||||
|
||||
@@ -68,7 +62,6 @@ final class ElevenLabsClientTest extends TestCase
|
||||
{
|
||||
$client = new ElevenLabsClient(
|
||||
new MockHttpClient(),
|
||||
'my-api-key',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
@@ -86,14 +79,15 @@ final class ElevenLabsClientTest extends TestCase
|
||||
]);
|
||||
$normalizer = new AudioNormalizer();
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$payload = $normalizer->normalize(Audio::fromFile(\dirname(__DIR__, 6).'/fixtures/audio.mp3'));
|
||||
|
||||
$client->request(new ElevenLabs('scribe_v1', [Capability::INPUT_AUDIO, Capability::OUTPUT_TEXT, Capability::SPEECH_TO_TEXT]), $payload);
|
||||
$client->request(new ElevenLabs('scribe_v1', [
|
||||
Capability::INPUT_AUDIO,
|
||||
Capability::OUTPUT_TEXT,
|
||||
Capability::SPEECH_TO_TEXT,
|
||||
]), $payload);
|
||||
|
||||
$this->assertSame(1, $httpClient->getRequestsCount());
|
||||
}
|
||||
@@ -107,14 +101,15 @@ final class ElevenLabsClientTest extends TestCase
|
||||
]);
|
||||
$normalizer = new AudioNormalizer();
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$payload = $normalizer->normalize(Audio::fromFile(\dirname(__DIR__, 6).'/fixtures/audio.mp3'));
|
||||
|
||||
$client->request(new ElevenLabs('scribe_v1_experimental', [Capability::INPUT_AUDIO, Capability::OUTPUT_TEXT, Capability::SPEECH_TO_TEXT]), $payload);
|
||||
$client->request(new ElevenLabs('scribe_v1_experimental', [
|
||||
Capability::INPUT_AUDIO,
|
||||
Capability::OUTPUT_TEXT,
|
||||
Capability::SPEECH_TO_TEXT,
|
||||
]), $payload);
|
||||
|
||||
$this->assertSame(1, $httpClient->getRequestsCount());
|
||||
}
|
||||
@@ -125,10 +120,7 @@ final class ElevenLabsClientTest extends TestCase
|
||||
new JsonMockResponse([]),
|
||||
]);
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$mockHttpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($mockHttpClient);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The payload must contain a "text" key');
|
||||
@@ -146,12 +138,9 @@ final class ElevenLabsClientTest extends TestCase
|
||||
new MockResponse($payload->asBinary()),
|
||||
]);
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$client->request(new ElevenLabs('eleven_multilingual_v2', [Capability::TEXT_TO_SPEECH], options: [
|
||||
$client->request(new ElevenLabs('eleven_multilingual_v2', [Capability::TEXT_TO_SPEECH], [
|
||||
'voice' => 'Dslrhjl3ZpzrctukrQSN',
|
||||
]), [
|
||||
'text' => 'foo',
|
||||
@@ -168,10 +157,7 @@ final class ElevenLabsClientTest extends TestCase
|
||||
new MockResponse($payload->asBinary()),
|
||||
]);
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$client->request(new ElevenLabs('eleven_multilingual_v2', [Capability::TEXT_TO_SPEECH]), [
|
||||
'text' => 'foo',
|
||||
@@ -190,12 +176,9 @@ final class ElevenLabsClientTest extends TestCase
|
||||
new MockResponse($payload->asBinary()),
|
||||
]);
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$result = $client->request(new ElevenLabs('eleven_multilingual_v2', capabilities: [Capability::TEXT_TO_SPEECH], options: [
|
||||
$result = $client->request(new ElevenLabs('eleven_multilingual_v2', [Capability::TEXT_TO_SPEECH], [
|
||||
'voice' => 'Dslrhjl3ZpzrctukrQSN',
|
||||
'stream' => true,
|
||||
]), [
|
||||
@@ -214,10 +197,7 @@ final class ElevenLabsClientTest extends TestCase
|
||||
new MockResponse($payload->asBinary()),
|
||||
]);
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$result = $client->request(new ElevenLabs('eleven_multilingual_v2', [Capability::TEXT_TO_SPEECH]), [
|
||||
'text' => 'foo',
|
||||
@@ -252,10 +232,7 @@ final class ElevenLabsClientTest extends TestCase
|
||||
return new MockResponse($payload->asBinary());
|
||||
});
|
||||
|
||||
$client = new ElevenLabsClient(
|
||||
$httpClient,
|
||||
'my-api-key',
|
||||
);
|
||||
$client = new ElevenLabsClient($httpClient);
|
||||
|
||||
$client->request(
|
||||
new ElevenLabs('eleven_multilingual_v2', [Capability::TEXT_TO_SPEECH]),
|
||||
|
||||
@@ -17,8 +17,10 @@ use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsResultConverter;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\Result\BinaryResult;
|
||||
use Symfony\AI\Platform\Result\InMemoryRawResult;
|
||||
use Symfony\AI\Platform\Result\StreamResult;
|
||||
use Symfony\AI\Platform\Result\TextResult;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
final class ElevenLabsConverterTest extends TestCase
|
||||
{
|
||||
@@ -48,6 +50,20 @@ final class ElevenLabsConverterTest extends TestCase
|
||||
$this->assertSame('Hello there', $result->getContent());
|
||||
}
|
||||
|
||||
public function testConvertTextToSpeechAsStreamResponse()
|
||||
{
|
||||
$converter = new ElevenLabsResultConverter(new MockHttpClient([], 'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream'));
|
||||
$rawResult = new InMemoryRawResult([], [], MockResponse::fromFile(\dirname(__DIR__).'/Tests/Fixtures/audio.mp3', [
|
||||
'url' => 'https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb/stream',
|
||||
]));
|
||||
|
||||
$result = $converter->convert($rawResult, [
|
||||
'stream' => true,
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(StreamResult::class, $result);
|
||||
}
|
||||
|
||||
public function testConvertTextToSpeechResponse()
|
||||
{
|
||||
$converter = new ElevenLabsResultConverter(new MockHttpClient());
|
||||
|
||||
BIN
src/platform/src/Bridge/ElevenLabs/Tests/Fixtures/audio.mp3
Normal file
BIN
src/platform/src/Bridge/ElevenLabs/Tests/Fixtures/audio.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user