From dd2846c1ce4b4e80378fbb731a9a6b036707eb72 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Tue, 5 Aug 2025 16:46:09 +0200 Subject: [PATCH 1/5] feat(agent): ElevenLabs TTS tool --- examples/.env | 3 + examples/misc/text-to-speech.php | 42 ++++++++++++++ src/agent/composer.json | 1 + src/agent/doc/index.rst | 2 + src/agent/src/Toolbox/Tool/ElevenLabs.php | 55 +++++++++++++++++++ .../tests/Toolbox/Tool/ElevenLabsTest.php | 43 +++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 examples/misc/text-to-speech.php create mode 100644 src/agent/src/Toolbox/Tool/ElevenLabs.php create mode 100644 src/agent/tests/Toolbox/Tool/ElevenLabsTest.php diff --git a/examples/.env b/examples/.env index fa6fdd1fa..7378a7b69 100644 --- a/examples/.env +++ b/examples/.env @@ -52,6 +52,9 @@ TAVILY_API_KEY= # For using Brave (tool) BRAVE_API_KEY= +# For using ElevenLabs (tool) +ELEVENLABS_API_KEY= + # For using MongoDB Atlas (store) MONGODB_URI= diff --git a/examples/misc/text-to-speech.php b/examples/misc/text-to-speech.php new file mode 100644 index 000000000..56ce589fe --- /dev/null +++ b/examples/misc/text-to-speech.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Agent\Agent; +use Symfony\AI\Agent\Toolbox\AgentProcessor; +use Symfony\AI\Agent\Toolbox\Tool\ElevenLabs; +use Symfony\AI\Agent\Toolbox\Toolbox; +use Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); +$model = new Gpt(Gpt::GPT_4O_MINI); + +$elevenLabs = new ElevenLabs( + http_client(), + env('ELEVENLABS_API_KEY'), + __DIR__.'/../tmp', + 'eleven_multilingual_v2', + 'Dslrhjl3ZpzrctukrQSN' // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN) +); + +$toolbox = new Toolbox([$elevenLabs], logger: logger()); +$toolProcessor = new AgentProcessor($toolbox); + +$agent = new Agent($platform, $model, inputProcessors: [$toolProcessor], outputProcessors: [$toolProcessor]); + +$messages = new MessageBag(Message::ofUser('Convert the following text to voice: "Hello world with voice!"')); +$result = $agent->call($messages); + +echo $result->getContent().\PHP_EOL; diff --git a/src/agent/composer.json b/src/agent/composer.json index 732492093..a7aedd754 100644 --- a/src/agent/composer.json +++ b/src/agent/composer.json @@ -41,6 +41,7 @@ "symfony/css-selector": "^6.4 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.1", "symfony/event-dispatcher": "^6.4 || ^7.1", + "symfony/filesystem": "^7.3", "symfony/http-foundation": "^6.4 || ^7.1" }, "config": { diff --git a/src/agent/doc/index.rst b/src/agent/doc/index.rst index 1c24b4865..f66da5b98 100644 --- a/src/agent/doc/index.rst +++ b/src/agent/doc/index.rst @@ -281,6 +281,7 @@ messages will be added to your MessageBag:: * `Weather Tool with Event Listener`_ * `Wikipedia Tool`_ * `YouTube Transcriber Tool`_ +* `ElevenLabs Text to Speech`_ Retrieval Augmented Generation (RAG) ------------------------------------ @@ -552,6 +553,7 @@ useful when certain interactions shouldn't be influenced by the memory context:: .. _`Weather Tool with Event Listener`: https://github.com/symfony/ai/blob/main/examples/toolbox/weather-event.php .. _`Wikipedia Tool`: https://github.com/symfony/ai/blob/main/examples/openai/toolcall-stream.php .. _`YouTube Transcriber Tool`: https://github.com/symfony/ai/blob/main/examples/openai/toolcall.php +.. _`ElevenLabs Text to Speech`: https://github.com/symfony/ai/blob/main/examples/misc/text-to-speech.php .. _`Store Component`: https://github.com/symfony/ai-store .. _`RAG with MongoDB`: https://github.com/symfony/ai/blob/main/examples/store/mongodb-similarity-search.php .. _`RAG with Pinecone`: https://github.com/symfony/ai/blob/main/examples/store/pinecone-similarity-search.php diff --git a/src/agent/src/Toolbox/Tool/ElevenLabs.php b/src/agent/src/Toolbox/Tool/ElevenLabs.php new file mode 100644 index 000000000..545869505 --- /dev/null +++ b/src/agent/src/Toolbox/Tool/ElevenLabs.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Toolbox\Tool; + +use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Guillaume Loulier + */ +#[AsTool('eleven_labs', description: 'convert text to speech / voice')] +final readonly class ElevenLabs +{ + public function __construct( + private HttpClientInterface $httpClient, + #[\SensitiveParameter] private string $apiKey, + private string $path, + private string $model, + private string $voice, + ) { + } + + public function __invoke(string $text): array + { + $response = $this->httpClient->request('POST', \sprintf('https://api.elevenlabs.io/v1/text-to-speech/%s?output_format=mp3_44100_128', $this->voice), [ + 'headers' => [ + 'xi-api-key' => $this->apiKey, + ], + 'json' => [ + 'text' => $text, + 'model_id' => $this->model, + ], + ]); + + $file = \sprintf('%s/%s.mp3', $this->path, uniqid()); + + $filesystem = new Filesystem(); + $filesystem->dumpFile($file, $response->getContent()); + + return [ + 'input' => $text, + 'path' => $file, + ]; + } +} diff --git a/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php b/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php new file mode 100644 index 000000000..530a348ef --- /dev/null +++ b/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Tests\Toolbox\Tool; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Agent\Toolbox\Tool\ElevenLabs; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; + +#[CoversClass(ElevenLabs::class)] +final class ElevenLabsTest extends TestCase +{ + public function testTextToSpeech() + { + $httpClient = new MockHttpClient( + new MockResponse(file_get_contents(__DIR__.'/../../../../../fixtures/audio.mp3'), [ + 'headers' => [ + 'Content-Type' => 'audio/mpeg', + ], + 'http_code' => 200, + ]), + ); + + $elevenLabs = new ElevenLabs($httpClient, 'foo', 'bar', 'baz', 'random'); + + $result = $elevenLabs('Hello World'); + + $this->assertCount(2, $result); + $this->assertSame('Hello World', $result['input']); + $this->assertNull($result['file']); + $this->assertSame(1, $httpClient->getRequestsCount()); + } +} From 626f1d505c49e3c0ec379d449d61ff5206f1ef66 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Tue, 5 Aug 2025 19:35:57 +0200 Subject: [PATCH 2/5] ref --- src/agent/src/Toolbox/Tool/ElevenLabs.php | 13 ++++++++++++- src/agent/tests/Toolbox/Tool/ElevenLabsTest.php | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/agent/src/Toolbox/Tool/ElevenLabs.php b/src/agent/src/Toolbox/Tool/ElevenLabs.php index 545869505..1d74f4127 100644 --- a/src/agent/src/Toolbox/Tool/ElevenLabs.php +++ b/src/agent/src/Toolbox/Tool/ElevenLabs.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Agent\Toolbox\Tool; +use Symfony\AI\Agent\Exception\RuntimeException; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; use Symfony\Component\Filesystem\Filesystem; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -18,7 +19,7 @@ /** * @author Guillaume Loulier */ -#[AsTool('eleven_labs', description: 'convert text to speech / voice')] +#[AsTool('eleven_labs', description: 'Convert text to speech / voice')] final readonly class ElevenLabs { public function __construct( @@ -30,8 +31,18 @@ public function __construct( ) { } + /** + * @return array{ + * input: string, + * path: string, + * } + */ public function __invoke(string $text): array { + if (!class_exists(Filesystem::class)) { + throw new RuntimeException('For using the ElevenLabs TTS tool, the symfony/filesystem package is required. Try running "composer require symfony/filesystem".'); + } + $response = $this->httpClient->request('POST', \sprintf('https://api.elevenlabs.io/v1/text-to-speech/%s?output_format=mp3_44100_128', $this->voice), [ 'headers' => [ 'xi-api-key' => $this->apiKey, diff --git a/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php b/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php index 530a348ef..c86fa5e0e 100644 --- a/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php +++ b/src/agent/tests/Toolbox/Tool/ElevenLabsTest.php @@ -37,7 +37,7 @@ public function testTextToSpeech() $this->assertCount(2, $result); $this->assertSame('Hello World', $result['input']); - $this->assertNull($result['file']); + $this->assertNotEmpty($result['path']); $this->assertSame(1, $httpClient->getRequestsCount()); } } From 716cdcc01f2da5630ed020c498b5b0710aac6211 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Tue, 5 Aug 2025 19:48:38 +0200 Subject: [PATCH 3/5] ref --- src/agent/composer.json | 3 ++- src/agent/src/Toolbox/Tool/ElevenLabs.php | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/agent/composer.json b/src/agent/composer.json index a7aedd754..ccb899c5e 100644 --- a/src/agent/composer.json +++ b/src/agent/composer.json @@ -42,7 +42,8 @@ "symfony/dom-crawler": "^6.4 || ^7.1", "symfony/event-dispatcher": "^6.4 || ^7.1", "symfony/filesystem": "^7.3", - "symfony/http-foundation": "^6.4 || ^7.1" + "symfony/http-foundation": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.1" }, "config": { "sort-packages": true diff --git a/src/agent/src/Toolbox/Tool/ElevenLabs.php b/src/agent/src/Toolbox/Tool/ElevenLabs.php index 1d74f4127..c90da24c1 100644 --- a/src/agent/src/Toolbox/Tool/ElevenLabs.php +++ b/src/agent/src/Toolbox/Tool/ElevenLabs.php @@ -14,12 +14,13 @@ use Symfony\AI\Agent\Exception\RuntimeException; use Symfony\AI\Agent\Toolbox\Attribute\AsTool; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Uid\Uuid; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Guillaume Loulier */ -#[AsTool('eleven_labs', description: 'Convert text to speech / voice')] +#[AsTool('text_to_speech', description: 'Convert text to speech / voice')] final readonly class ElevenLabs { public function __construct( @@ -29,6 +30,13 @@ public function __construct( private string $model, private string $voice, ) { + if (!class_exists(Filesystem::class)) { + throw new RuntimeException('For using the ElevenLabs TTS tool, the symfony/filesystem package is required. Try running "composer require symfony/filesystem".'); + } + + if (!class_exists(Uuid::class)) { + throw new RuntimeException('For using the ElevenLabs TTS tool, the symfony/uid package is required. Try running "composer require symfony/uid".'); + } } /** @@ -39,10 +47,6 @@ public function __construct( */ public function __invoke(string $text): array { - if (!class_exists(Filesystem::class)) { - throw new RuntimeException('For using the ElevenLabs TTS tool, the symfony/filesystem package is required. Try running "composer require symfony/filesystem".'); - } - $response = $this->httpClient->request('POST', \sprintf('https://api.elevenlabs.io/v1/text-to-speech/%s?output_format=mp3_44100_128', $this->voice), [ 'headers' => [ 'xi-api-key' => $this->apiKey, @@ -53,7 +57,7 @@ public function __invoke(string $text): array ], ]); - $file = \sprintf('%s/%s.mp3', $this->path, uniqid()); + $file = \sprintf('%s/%s.mp3', $this->path, Uuid::v4()->toRfc4122()); $filesystem = new Filesystem(); $filesystem->dumpFile($file, $response->getContent()); From b379308efbf9e3674f469bcd99605a67ab238fb1 Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Tue, 5 Aug 2025 21:00:08 +0200 Subject: [PATCH 4/5] deps --- src/agent/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/composer.json b/src/agent/composer.json index ccb899c5e..a6ed0e1a9 100644 --- a/src/agent/composer.json +++ b/src/agent/composer.json @@ -41,7 +41,7 @@ "symfony/css-selector": "^6.4 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.1", "symfony/event-dispatcher": "^6.4 || ^7.1", - "symfony/filesystem": "^7.3", + "symfony/filesystem": "^6.4 || ^7.1", "symfony/http-foundation": "^6.4 || ^7.1", "symfony/uid": "^6.4 || ^7.1" }, From ef185a895f981f5c7e0e275b1d1c1188a884269c Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Thu, 7 Aug 2025 10:55:00 +0200 Subject: [PATCH 5/5] doc --- src/agent/src/Toolbox/Tool/ElevenLabs.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agent/src/Toolbox/Tool/ElevenLabs.php b/src/agent/src/Toolbox/Tool/ElevenLabs.php index c90da24c1..ab91fb889 100644 --- a/src/agent/src/Toolbox/Tool/ElevenLabs.php +++ b/src/agent/src/Toolbox/Tool/ElevenLabs.php @@ -19,6 +19,8 @@ /** * @author Guillaume Loulier + * + * @see https://elevenlabs.io/ */ #[AsTool('text_to_speech', description: 'Convert text to speech / voice')] final readonly class ElevenLabs