Skip to content

Commit 8dd5cd5

Browse files
committed
feat(core): voice package
1 parent 09215b4 commit 8dd5cd5

File tree

19 files changed

+396
-4
lines changed

19 files changed

+396
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Symfony AI consists of several lower and higher level **components** and the res
1313
* **[Agent](src/agent/README.md)**: Framework for building AI agents that can interact with users and perform tasks.
1414
* **[Chat](src/chat/README.md)**: An unified interface to send messages to agents and store long-term context.
1515
* **[Store](src/store/README.md)**: Data storage abstraction with indexing and retrieval for AI applications.
16+
* **[Voice](src/voice/README.md)**: An unified interface to provide TTS / STT / STS for agents and more.
1617
* **Bundles**
1718
* **[AI Bundle](src/ai-bundle/README.md)**: Symfony integration for AI Platform, Store and Agent components.
1819
* **[MCP Bundle](src/mcp-bundle/README.md)**: Symfony integration for official MCP SDK, allowing them to act as MCP servers or clients.

docs/components/voice.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Symfony AI - Voice Component
2+
============================
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
14+
use Symfony\AI\Platform\Message\Content\Text;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$elevenLabsPlatform = PlatformFactory::create(
21+
apiKey: env('ELEVEN_LABS_API_KEY'),
22+
httpClient: http_client(),
23+
);
24+
25+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), httpClient: http_client());
26+
27+
$agent = new Agent($platform, 'gpt-4o');
28+
$answer = $agent->call(new MessageBag(
29+
Message::ofUser('Hello'),
30+
));
31+
32+
$result = $platform->invoke('eleven_multilingual_v2', new Text('Hello world'), [
33+
'voice' => 'Dslrhjl3ZpzrctukrQSN', // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
34+
]);
35+
36+
echo $answer->asVoice();

src/agent/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"phpstan/phpdoc-parser": "^2.1",
2626
"psr/log": "^3.0",
2727
"symfony/ai-platform": "@dev",
28+
"symfony/ai-voice": "@dev",
2829
"symfony/clock": "^7.3|^8.0",
2930
"symfony/http-client": "^7.3|^8.0",
3031
"symfony/property-access": "^7.3|^8.0",

src/agent/src/Agent.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\AI\Platform\Message\MessageBag;
1818
use Symfony\AI\Platform\PlatformInterface;
1919
use Symfony\AI\Platform\Result\ResultInterface;
20+
use Symfony\AI\Voice\VoiceProviderInterface;
2021

2122
/**
2223
* @author Christopher Hertel <[email protected]>
@@ -33,20 +34,28 @@ final class Agent implements AgentInterface
3334
*/
3435
private readonly array $outputProcessors;
3536

37+
/**
38+
* @var VoiceProviderInterface[]
39+
*/
40+
private array $voiceProviders;
41+
3642
/**
3743
* @param InputProcessorInterface[] $inputProcessors
3844
* @param OutputProcessorInterface[] $outputProcessors
45+
* @param VoiceProviderInterface[] $voiceProviders
3946
* @param non-empty-string $model
4047
*/
4148
public function __construct(
4249
private readonly PlatformInterface $platform,
4350
private readonly string $model,
4451
iterable $inputProcessors = [],
4552
iterable $outputProcessors = [],
53+
iterable $voiceProviders = [],
4654
private readonly string $name = 'agent',
4755
) {
4856
$this->inputProcessors = $this->initializeProcessors($inputProcessors, InputProcessorInterface::class);
4957
$this->outputProcessors = $this->initializeProcessors($outputProcessors, OutputProcessorInterface::class);
58+
$this->voiceProviders = $this->initializeVoiceProviders($voiceProviders);
5059
}
5160

5261
public function getModel(): string
@@ -69,7 +78,7 @@ public function getName(): string
6978
public function call(MessageBag $messages, array $options = []): ResultInterface
7079
{
7180
$input = new Input($this->getModel(), $messages, $options);
72-
array_map(fn (InputProcessorInterface $processor) => $processor->processInput($input), $this->inputProcessors);
81+
array_map(static fn (InputProcessorInterface $processor) => $processor->processInput($input), $this->inputProcessors);
7382

7483
$model = $input->getModel();
7584
$messages = $input->getMessageBag();
@@ -78,7 +87,8 @@ public function call(MessageBag $messages, array $options = []): ResultInterface
7887
$result = $this->platform->invoke($model, $messages, $options)->getResult();
7988

8089
$output = new Output($model, $result, $messages, $options);
81-
array_map(fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors);
90+
array_map(static fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors);
91+
array_map(static fn (VoiceProviderInterface $provider) => $provider->addVoice($output), $this->voiceProviders);
8292

8393
return $output->getResult();
8494
}
@@ -103,4 +113,24 @@ private function initializeProcessors(iterable $processors, string $interface):
103113

104114
return $processors instanceof \Traversable ? iterator_to_array($processors) : $processors;
105115
}
116+
117+
/**
118+
* @param VoiceProviderInterface[] $providers
119+
*
120+
* @return VoiceProviderInterface[]
121+
*/
122+
private function initializeVoiceProviders(iterable $providers): array
123+
{
124+
foreach ($providers as $provider) {
125+
if (!$provider instanceof VoiceProviderInterface) {
126+
throw new InvalidArgumentException(\sprintf('Voice provider "%s" must implement "%s".', $provider::class, VoiceProviderInterface::class));
127+
}
128+
129+
if ($provider instanceof AgentAwareInterface) {
130+
$provider->setAgent($this);
131+
}
132+
}
133+
134+
return $providers instanceof \Traversable ? iterator_to_array($providers) : $providers;
135+
}
106136
}

src/agent/src/Output.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function __construct(
2727
private ResultInterface $result,
2828
private readonly MessageBag $messageBag,
2929
private readonly array $options = [],
30+
private ?Voice $voice = null,
3031
) {
3132
}
3233

@@ -57,4 +58,14 @@ public function getOptions(): array
5758
{
5859
return $this->options;
5960
}
61+
62+
public function setVoice(?Voice $voice): void
63+
{
64+
$this->voice = $voice;
65+
}
66+
67+
public function getVoice(): ?Voice
68+
{
69+
return $this->voice;
70+
}
6071
}

src/ai-bundle/config/options.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,18 @@
960960
->end()
961961
->end()
962962
->end()
963+
->arrayNode('voice')
964+
->children()
965+
->arrayNode('eleven_labs')
966+
->useAttributeAsKey('name')
967+
->arrayPrototype()
968+
->children()
969+
->stringNode('model')->cannotBeEmpty()->end()
970+
->end()
971+
->end()
972+
->end()
973+
->end()
974+
->end()
963975
->arrayNode('vectorizer')
964976
->info('Vectorizers for converting strings to Vector objects and transforming TextDocument arrays to VectorDocument arrays')
965977
->useAttributeAsKey('name')

src/ai-bundle/src/AiBundle.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@
102102
use Symfony\AI\Store\Indexer;
103103
use Symfony\AI\Store\IndexerInterface;
104104
use Symfony\AI\Store\StoreInterface;
105+
use Symfony\AI\Voice\Bridge\ElevenLabs\VoiceProvider as ElevenLabsVoiceProvider;
106+
use Symfony\AI\Voice\VoiceProviderInterface;
105107
use Symfony\Component\Clock\ClockInterface;
106108
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
107109
use Symfony\Component\DependencyInjection\Attribute\Target;
@@ -248,6 +250,10 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
248250
}
249251
}
250252

253+
foreach ($config['voice'] as $voiceProvider => $provider) {
254+
$this->processVoiceProviderConfig($voiceProvider, $provider, $builder);
255+
}
256+
251257
foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
252258
$this->processVectorizerConfig($vectorizerName, $vectorizer, $builder);
253259
}
@@ -917,8 +923,9 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
917923
$agentDefinition
918924
->setArgument(2, []) // placeholder until ProcessorCompilerPass process.
919925
->setArgument(3, []) // placeholder until ProcessorCompilerPass process.
920-
->setArgument(4, $name)
921-
->setArgument(5, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
926+
->setArgument(4, []) // placeholder until VoiceProviderCompilerPass process.
927+
->setArgument(5, $name)
928+
->setArgument(6, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
922929
;
923930

924931
$container->setDefinition($agentId, $agentDefinition);
@@ -1760,6 +1767,30 @@ private function processChatConfig(string $name, array $configuration, Container
17601767
$container->registerAliasForArgument('ai.chat.'.$name, ChatInterface::class, $name);
17611768
}
17621769

1770+
/**
1771+
* @param array<string, mixed> $providers
1772+
*/
1773+
private function processVoiceProviderConfig(string $name, array $providers, ContainerBuilder $container): void
1774+
{
1775+
if ('eleven_labs' === $name) {
1776+
foreach ($providers as $type => $config) {
1777+
$definition = new Definition(ElevenLabsVoiceProvider::class);
1778+
$definition
1779+
->setLazy(true)
1780+
->setArguments([
1781+
new Reference('ai.platform.eleven_labs'),
1782+
$config['model'],
1783+
])
1784+
->addTag('proxy', ['interface' => VoiceProviderInterface::class])
1785+
->addTag('ai.voice.provider');
1786+
1787+
$container->setDefinition('ai.voice.eleven_labs.'.$name, $definition);
1788+
$container->registerAliasForArgument('ai.voice.'.$type.'.'.$name, VoiceProviderInterface::class, $name);
1789+
$container->registerAliasForArgument('ai.voice.'.$type.'.'.$name, VoiceProviderInterface::class, $type.'_'.$name);
1790+
}
1791+
}
1792+
}
1793+
17631794
/**
17641795
* @param array<string, mixed> $config
17651796
*/
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\AiBundle\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* @author Guillaume Loulier <[email protected]>
19+
*/
20+
final class VoiceProviderCompilerPass implements CompilerPassInterface
21+
{
22+
public function process(ContainerBuilder $container): void
23+
{
24+
$voiceProviders = $container->findTaggedServiceIds('ai.voice.provider');
25+
}
26+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\Result;
13+
14+
/**
15+
* @author Guillaume Loulier <[email protected]>
16+
*/
17+
trait VoiceAwareTrait
18+
{
19+
20+
}

0 commit comments

Comments
 (0)