Skip to content

Commit f011c3e

Browse files
committed
ref
1 parent 8dd5cd5 commit f011c3e

File tree

23 files changed

+178
-208
lines changed

23 files changed

+178
-208
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ 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.
1716
* **Bundles**
1817
* **[AI Bundle](src/ai-bundle/README.md)**: Symfony integration for AI Platform, Store and Agent components.
1918
* **[MCP Bundle](src/mcp-bundle/README.md)**: Symfony integration for official MCP SDK, allowing them to act as MCP servers or clients.

docs/components/platform.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,9 @@ This allows fast and isolated testing of AI-powered features without relying on
501501

502502
This requires `cURL` and the `ext-curl` extension to be installed.
503503

504+
Adding Voice
505+
~~~~~~~~~~~~
506+
504507
Code Examples
505508
~~~~~~~~~~~~~
506509

docs/components/voice.rst

Lines changed: 0 additions & 2 deletions
This file was deleted.

examples/voice/agent-eleven-labs-voice.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@
3333
'voice' => 'Dslrhjl3ZpzrctukrQSN', // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
3434
]);
3535

36-
echo $answer->asVoice();
36+
echo $result->asVoice();

src/agent/composer.json

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

src/agent/src/Agent.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +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;
20+
use Symfony\AI\Platform\Voice\SpeechProviderInterface;
2121

2222
/**
2323
* @author Christopher Hertel <[email protected]>
@@ -37,7 +37,7 @@ final class Agent implements AgentInterface
3737
/**
3838
* @var VoiceProviderInterface[]
3939
*/
40-
private array $voiceProviders;
40+
private readonly array $voiceProviders;
4141

4242
/**
4343
* @param InputProcessorInterface[] $inputProcessors
@@ -88,7 +88,7 @@ public function call(MessageBag $messages, array $options = []): ResultInterface
8888

8989
$output = new Output($model, $result, $messages, $options);
9090
array_map(static fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors);
91-
array_map(static fn (VoiceProviderInterface $provider) => $provider->addVoice($output), $this->voiceProviders);
91+
array_map(static fn (SpeechProviderInterface $provider) => $provider->addVoice($output), $this->voiceProviders);
9292

9393
return $output->getResult();
9494
}
@@ -122,8 +122,8 @@ private function initializeProcessors(iterable $processors, string $interface):
122122
private function initializeVoiceProviders(iterable $providers): array
123123
{
124124
foreach ($providers as $provider) {
125-
if (!$provider instanceof VoiceProviderInterface) {
126-
throw new InvalidArgumentException(\sprintf('Voice provider "%s" must implement "%s".', $provider::class, VoiceProviderInterface::class));
125+
if (!$provider instanceof SpeechProviderInterface) {
126+
throw new InvalidArgumentException(\sprintf('Speech provider "%s" must implement "%s".', $provider::class, SpeechProviderInterface::class));
127127
}
128128

129129
if ($provider instanceof AgentAwareInterface) {

src/agent/src/Output.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\AI\Platform\Message\MessageBag;
1515
use Symfony\AI\Platform\Result\ResultInterface;
16+
use Symfony\AI\Platform\Speech\Speech;
1617

1718
/**
1819
* @author Christopher Hertel <[email protected]>
@@ -27,7 +28,7 @@ public function __construct(
2728
private ResultInterface $result,
2829
private readonly MessageBag $messageBag,
2930
private readonly array $options = [],
30-
private ?Voice $voice = null,
31+
private ?Speech $speech = null,
3132
) {
3233
}
3334

@@ -59,13 +60,13 @@ public function getOptions(): array
5960
return $this->options;
6061
}
6162

62-
public function setVoice(?Voice $voice): void
63+
public function setSpeech(?Speech $speech): void
6364
{
64-
$this->voice = $voice;
65+
$this->speech = $speech;
6566
}
6667

67-
public function getVoice(): ?Voice
68+
public function getSpeech(): ?Speech
6869
{
69-
return $this->voice;
70+
return $this->speech;
7071
}
7172
}

src/ai-bundle/src/AiBundle.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
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;
105+
use Symfony\AI\Voice\Bridge\ElevenLabs\ElevenLabsSpeechProvider as ElevenLabsVoiceProvider;
106106
use Symfony\AI\Voice\VoiceProviderInterface;
107107
use Symfony\Component\Clock\ClockInterface;
108108
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
@@ -1774,7 +1774,7 @@ private function processVoiceProviderConfig(string $name, array $providers, Cont
17741774
{
17751775
if ('eleven_labs' === $name) {
17761776
foreach ($providers as $type => $config) {
1777-
$definition = new Definition(ElevenLabsVoiceProvider::class);
1777+
$definition = new Definition(ElevenLabsSpeechProvider::class);
17781778
$definition
17791779
->setLazy(true)
17801780
->setArguments([
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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\Voice\Bridge\ElevenLabs;
13+
14+
use Symfony\AI\Agent\Output;
15+
use Symfony\AI\Platform\Speech\Speech;
16+
use Symfony\AI\Platform\Speech\SpeechListenerInterface;
17+
use Symfony\AI\Platform\Speech\SpeechProviderInterface;
18+
use Symfony\AI\Platform\Platform;
19+
20+
/**
21+
* @author Guillaume Loulier <[email protected]>
22+
*/
23+
final class ElevenLabsSpeechProvider implements SpeechProviderInterface, SpeechListenerInterface
24+
{
25+
public const ELEVEN_LABS_STT_MODEL = 'eleven_labs.stt_model';
26+
27+
public function __construct(
28+
private readonly Platform $platform,
29+
private readonly string $model,
30+
) {
31+
}
32+
33+
public function addSpeech(Output $output): void
34+
{
35+
$result = $output->getResult();
36+
37+
$voice = $this->platform->invoke($this->model, $result->getContent());
38+
39+
$output->setSpeech(new Speech($result->getContent(), $voice));
40+
}
41+
42+
public function listen(object|array|string $input, array $options): Speech
43+
{
44+
$model = $options[self::ELEVEN_LABS_STT_MODEL];
45+
46+
unset($options[self::ELEVEN_LABS_STT_MODEL]);
47+
48+
$text = $this->platform->invoke($model, $input);
49+
50+
return new Speech($input, $text);
51+
}
52+
53+
public function supportListening(object|array|string $input, array $options): bool
54+
{
55+
return \array_key_exists(self::ELEVEN_LABS_STT_MODEL, $options);
56+
}
57+
}

src/platform/src/Platform.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
1919
use Symfony\AI\Platform\Result\DeferredResult;
2020
use Symfony\AI\Platform\Result\RawResultInterface;
21+
use Symfony\AI\Platform\Speech\SpeechListenerInterface;
2122

2223
/**
2324
* @author Christopher Hertel <[email protected]>
@@ -34,20 +35,28 @@ final class Platform implements PlatformInterface
3435
*/
3536
private readonly array $resultConverters;
3637

38+
/**
39+
* @var SpeechListenerInterface[]
40+
*/
41+
private readonly iterable $speechListeners;
42+
3743
/**
3844
* @param iterable<ModelClientInterface> $modelClients
3945
* @param iterable<ResultConverterInterface> $resultConverters
46+
* @param iterable<SpeechListenerInterface> $speechListeners
4047
*/
4148
public function __construct(
4249
iterable $modelClients,
4350
iterable $resultConverters,
51+
iterable $speechListeners,
4452
private readonly ModelCatalogInterface $modelCatalog,
4553
private ?Contract $contract = null,
4654
private readonly ?EventDispatcherInterface $eventDispatcher = null,
4755
) {
4856
$this->contract = $contract ?? Contract::create();
4957
$this->modelClients = $modelClients instanceof \Traversable ? iterator_to_array($modelClients) : $modelClients;
5058
$this->resultConverters = $resultConverters instanceof \Traversable ? iterator_to_array($resultConverters) : $resultConverters;
59+
$this->speechListeners = $speechListeners instanceof \Traversable ? iterator_to_array($speechListeners) : $speechListeners;
5160
}
5261

5362
public function invoke(string $model, array|string|object $input, array $options = []): DeferredResult
@@ -65,8 +74,9 @@ public function invoke(string $model, array|string|object $input, array $options
6574
}
6675

6776
$result = $this->convertResult($model, $this->doInvoke($model, $payload, $options), $options);
77+
$finalResult = $this->addSpeech($result, $payload, $options);
6878

69-
$event = new ResultEvent($model, $result, $options);
79+
$event = new ResultEvent($model, $finalResult, $options);
7080
$this->eventDispatcher?->dispatch($event);
7181

7282
return $event->getDeferredResult();
@@ -105,4 +115,21 @@ private function convertResult(Model $model, RawResultInterface $result, array $
105115

106116
throw new RuntimeException(\sprintf('No ResultConverter registered for model "%s" with given input.', $model::class));
107117
}
118+
119+
private function addSpeech(DeferredResult $result, array|string $payload, array $options): DeferredResult
120+
{
121+
if ([] === $this->speechListeners) {
122+
return $result;
123+
}
124+
125+
foreach ($this->speechListeners as $speechListener) {
126+
if ($speechListener->supportListening($payload, $options)) {
127+
$result->setSpeech($speechListener->listen($payload, $options));
128+
129+
return $result;
130+
}
131+
}
132+
133+
throw new RuntimeException('No SpeechListener registered.');
134+
}
108135
}

0 commit comments

Comments
 (0)