diff --git a/UPGRADE.md b/UPGRADE.md index 4584df8742..4409c125f3 100644 --- a/UPGRADE.md +++ b/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 ======================= diff --git a/examples/elevenlabs/speech-to-text.php b/examples/elevenlabs/speech-to-text.php index 2490c9da38..5f76015a13 100644 --- a/examples/elevenlabs/speech-to-text.php +++ b/examples/elevenlabs/speech-to-text.php @@ -14,10 +14,7 @@ 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')); diff --git a/examples/elevenlabs/text-to-speech-as-stream.php b/examples/elevenlabs/text-to-speech-as-stream.php index 27e7897107..757aceb24e 100644 --- a/examples/elevenlabs/text-to-speech-as-stream.php +++ b/examples/elevenlabs/text-to-speech-as-stream.php @@ -14,10 +14,7 @@ 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) diff --git a/examples/elevenlabs/text-to-speech.php b/examples/elevenlabs/text-to-speech.php index da8994194e..8318932fa8 100644 --- a/examples/elevenlabs/text-to-speech.php +++ b/examples/elevenlabs/text-to-speech.php @@ -14,10 +14,7 @@ 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) diff --git a/src/ai-bundle/CHANGELOG.md b/src/ai-bundle/CHANGELOG.md index b3f3678606..14fba9c156 100644 --- a/src/ai-bundle/CHANGELOG.md +++ b/src/ai-bundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.5 +--- + + * [BC BREAK] The `host_url` configuration key for `ElevenLabs` has been renamed `endpoint` + 0.4 --- diff --git a/src/ai-bundle/config/platform/elevenlabs.php b/src/ai-bundle/config/platform/elevenlabs.php index 945a2de2f6..9b2bd1c950 100644 --- a/src/ai-bundle/config/platform/elevenlabs.php +++ b/src/ai-bundle/config/platform/elevenlabs.php @@ -16,8 +16,8 @@ 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') diff --git a/src/ai-bundle/config/services.php b/src/ai-bundle/config/services.php index 718c612d0e..36c4b6c06f 100644 --- a/src/ai-bundle/config/services.php +++ b/src/ai-bundle/config/services.php @@ -36,6 +36,7 @@ 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 @@ ->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 @@ ->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) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 18d658d955..45ae22d227 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -605,13 +605,29 @@ private function processPlatformConfig(string $type, array $platform, ContainerB 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 @@ private function processPlatformConfig(string $type, array $platform, ContainerB ->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]) diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index cc21e5a01f..3797a29f47 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -3895,7 +3895,7 @@ public function testToolboxWithoutExplicitToolsDefined() $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 @@ public function testElevenLabsPlatformCanBeRegistered() $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 @@ public function testElevenLabsPlatformCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithApiCatalogCanBeRegistered() $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 @@ public function testElevenLabsPlatformWithApiCatalogCanBeRegistered() $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 @@ private function getFullConfig(): array 'api_key' => 'foo', ], 'elevenlabs' => [ - 'host' => 'https://api.elevenlabs.io/v1', + 'endpoint' => 'https://api.elevenlabs.io/v1', 'api_key' => 'elevenlabs_key_full', ], 'failover' => [ diff --git a/src/platform/src/Bridge/ElevenLabs/CHANGELOG.md b/src/platform/src/Bridge/ElevenLabs/CHANGELOG.md index ff3d572782..4354635baf 100644 --- a/src/platform/src/Bridge/ElevenLabs/CHANGELOG.md +++ b/src/platform/src/Bridge/ElevenLabs/CHANGELOG.md @@ -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 --- diff --git a/src/platform/src/Bridge/ElevenLabs/ElevenLabsApiCatalog.php b/src/platform/src/Bridge/ElevenLabs/ElevenLabsApiCatalog.php index 625535ba34..7f625387e3 100644 --- a/src/platform/src/Bridge/ElevenLabs/ElevenLabsApiCatalog.php +++ b/src/platform/src/Bridge/ElevenLabs/ElevenLabsApiCatalog.php @@ -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 @@ public function getModel(string $modelName): ElevenLabs 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(); diff --git a/src/platform/src/Bridge/ElevenLabs/ElevenLabsClient.php b/src/platform/src/Bridge/ElevenLabs/ElevenLabsClient.php index a56fefc59d..8e21ffc932 100644 --- a/src/platform/src/Bridge/ElevenLabs/ElevenLabsClient.php +++ b/src/platform/src/Bridge/ElevenLabs/ElevenLabsClient.php @@ -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 @@ public function request(Model $model, array|string $payload, array $options = [] 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 @@ public function request(Model $model, array|string $payload, array $options = [] */ 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 @@ private function doTextToSpeechRequest(Model $model, array|string $payload, arra $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(), diff --git a/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php b/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php index 9625193b9e..a21e9692e3 100644 --- a/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php +++ b/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php @@ -43,9 +43,9 @@ public function convert(RawHttpResult|RawResultInterface $result, array $options $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.'), }; } diff --git a/src/platform/src/Bridge/ElevenLabs/PlatformFactory.php b/src/platform/src/Bridge/ElevenLabs/PlatformFactory.php index a1ed7249d8..19d5b6dad6 100644 --- a/src/platform/src/Bridge/ElevenLabs/PlatformFactory.php +++ b/src/platform/src/Bridge/ElevenLabs/PlatformFactory.php @@ -17,6 +17,7 @@ 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 @@ 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 @@ public static function create( ): 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(), diff --git a/src/platform/src/Bridge/ElevenLabs/README.md b/src/platform/src/Bridge/ElevenLabs/README.md index 75cf019a81..8993ac73ae 100644 --- a/src/platform/src/Bridge/ElevenLabs/README.md +++ b/src/platform/src/Bridge/ElevenLabs/README.md @@ -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 --------- diff --git a/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsApiCatalogTest.php b/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsApiCatalogTest.php index 8ce9ff127c..4e807d514a 100644 --- a/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsApiCatalogTest.php +++ b/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsApiCatalogTest.php @@ -27,7 +27,7 @@ public function testModelCatalogCannotReturnModelFromApiWhenUndefined() 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 @@ public function testModelCatalogCannotReturnUnsupportedModelFromApi() ]), ]); - $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 @@ public function testModelCatalogCanReturnSpecificTtsModelFromApi() ]), ]); - $modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo'); + $modelCatalog = new ElevenLabsApiCatalog($httpClient); $model = $modelCatalog->getModel('foo'); @@ -102,7 +102,7 @@ public function testModelCatalogCanReturnSpecificSttModelFromApi() ]), ]); - $modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo'); + $modelCatalog = new ElevenLabsApiCatalog($httpClient); $model = $modelCatalog->getModel('foo'); @@ -135,7 +135,7 @@ public function testModelCatalogCanReturnModelsFromApi() ]), ]); - $modelCatalog = new ElevenLabsApiCatalog($httpClient, 'foo'); + $modelCatalog = new ElevenLabsApiCatalog($httpClient); $models = $modelCatalog->getModels(); diff --git a/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsClientTest.php b/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsClientTest.php index 31be2b0696..aeaa4d48f4 100644 --- a/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsClientTest.php +++ b/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsClientTest.php @@ -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 @@ public function testClientCannotPerformWithInvalidModel() ]); $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 @@ public function testClientCannotPerformSpeechToTextRequestWithInvalidPayload() { $client = new ElevenLabsClient( new MockHttpClient(), - 'my-api-key', ); $this->expectException(InvalidArgumentException::class); @@ -86,14 +79,15 @@ public function testClientCanPerformSpeechToTextRequest() ]); $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 @@ public function testClientCanPerformSpeechToTextRequestWithExperimentalModel() ]); $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 @@ public function testClientCannotPerformTextToSpeechRequestWithoutValidPayload() 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 @@ public function testClientCanPerformTextToSpeechRequest() 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 @@ public function testClientCanPerformTextToSpeechRequestWhenVoiceKeyIsProvidedAsR 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 @@ public function testClientCanPerformTextToSpeechRequestAsStream() 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 @@ public function testClientCanPerformTextToSpeechRequestAsStreamVoiceKeyIsProvide 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 @@ public function testClientCanPerformTextToSpeechRequestWithExtraApiOptions() 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]), diff --git a/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsConverterTest.php b/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsConverterTest.php index 9890043c7d..593dd7b722 100644 --- a/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsConverterTest.php +++ b/src/platform/src/Bridge/ElevenLabs/Tests/ElevenLabsConverterTest.php @@ -17,8 +17,10 @@ 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 @@ public function getInfo(): string $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()); diff --git a/src/platform/src/Bridge/ElevenLabs/Tests/Fixtures/audio.mp3 b/src/platform/src/Bridge/ElevenLabs/Tests/Fixtures/audio.mp3 new file mode 100644 index 0000000000..509aa0fc4c Binary files /dev/null and b/src/platform/src/Bridge/ElevenLabs/Tests/Fixtures/audio.mp3 differ