Skip to content

Commit dcae952

Browse files
committed
ref
1 parent f011c3e commit dcae952

File tree

13 files changed

+171
-110
lines changed

13 files changed

+171
-110
lines changed

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,35 @@
1010
*/
1111

1212
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsSpeechProvider;
1314
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
14-
use Symfony\AI\Platform\Message\Content\Text;
1515
use Symfony\AI\Platform\Message\Message;
1616
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\AI\Platform\Speech\SpeechConfiguration;
18+
use Symfony\AI\Platform\Speech\SpeechProviderListener;
19+
use Symfony\Component\EventDispatcher\EventDispatcher;
1720

1821
require_once dirname(__DIR__).'/bootstrap.php';
1922

20-
$elevenLabsPlatform = PlatformFactory::create(
21-
apiKey: env('ELEVEN_LABS_API_KEY'),
22-
httpClient: http_client(),
23-
);
23+
$eventDispatcher = new EventDispatcher();
24+
$eventDispatcher->addSubscriber(new SpeechProviderListener([
25+
new ElevenLabsSpeechProvider(PlatformFactory::create(
26+
apiKey: env('ELEVEN_LABS_API_KEY'),
27+
httpClient: http_client(),
28+
), new SpeechConfiguration(
29+
'eleven_multilingual_v2',
30+
'Dslrhjl3ZpzrctukrQSN', // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
31+
'eleven_multilingual_v2'
32+
)),
33+
]));
2434

2535
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), httpClient: http_client());
2636

2737
$agent = new Agent($platform, 'gpt-4o');
2838
$answer = $agent->call(new MessageBag(
2939
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)
40+
), [
41+
ElevenLabsSpeechProvider::ELEVEN_LABS_STT_MODEL => true,
3442
]);
3543

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

src/agent/src/Agent.php

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

2221
/**
2322
* @author Christopher Hertel <[email protected]>
@@ -34,28 +33,20 @@ final class Agent implements AgentInterface
3433
*/
3534
private readonly array $outputProcessors;
3635

37-
/**
38-
* @var VoiceProviderInterface[]
39-
*/
40-
private readonly array $voiceProviders;
41-
4236
/**
4337
* @param InputProcessorInterface[] $inputProcessors
4438
* @param OutputProcessorInterface[] $outputProcessors
45-
* @param VoiceProviderInterface[] $voiceProviders
4639
* @param non-empty-string $model
4740
*/
4841
public function __construct(
4942
private readonly PlatformInterface $platform,
5043
private readonly string $model,
5144
iterable $inputProcessors = [],
5245
iterable $outputProcessors = [],
53-
iterable $voiceProviders = [],
5446
private readonly string $name = 'agent',
5547
) {
5648
$this->inputProcessors = $this->initializeProcessors($inputProcessors, InputProcessorInterface::class);
5749
$this->outputProcessors = $this->initializeProcessors($outputProcessors, OutputProcessorInterface::class);
58-
$this->voiceProviders = $this->initializeVoiceProviders($voiceProviders);
5950
}
6051

6152
public function getModel(): string
@@ -88,7 +79,6 @@ public function call(MessageBag $messages, array $options = []): ResultInterface
8879

8980
$output = new Output($model, $result, $messages, $options);
9081
array_map(static fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors);
91-
array_map(static fn (SpeechProviderInterface $provider) => $provider->addVoice($output), $this->voiceProviders);
9282

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

114104
return $processors instanceof \Traversable ? iterator_to_array($processors) : $processors;
115105
}
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 SpeechProviderInterface) {
126-
throw new InvalidArgumentException(\sprintf('Speech provider "%s" must implement "%s".', $provider::class, SpeechProviderInterface::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-
}
136106
}

src/ai-bundle/src/AiBundle.php

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
5555
use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory as DeepSeekPlatformFactory;
5656
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory as DockerModelRunnerPlatformFactory;
57+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsSpeechProvider;
5758
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
5859
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory as GeminiPlatformFactory;
5960
use Symfony\AI\Platform\Bridge\HuggingFace\PlatformFactory as HuggingFacePlatformFactory;
@@ -75,6 +76,8 @@
7576
use Symfony\AI\Platform\Platform;
7677
use Symfony\AI\Platform\PlatformInterface;
7778
use Symfony\AI\Platform\ResultConverterInterface;
79+
use Symfony\AI\Platform\Speech\SpeechConfiguration;
80+
use Symfony\AI\Platform\Speech\SpeechProviderInterface;
7881
use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureSearchStore;
7982
use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore;
8083
use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore;
@@ -102,8 +105,6 @@
102105
use Symfony\AI\Store\Indexer;
103106
use Symfony\AI\Store\IndexerInterface;
104107
use Symfony\AI\Store\StoreInterface;
105-
use Symfony\AI\Voice\Bridge\ElevenLabs\ElevenLabsSpeechProvider as ElevenLabsVoiceProvider;
106-
use Symfony\AI\Voice\VoiceProviderInterface;
107108
use Symfony\Component\Clock\ClockInterface;
108109
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
109110
use Symfony\Component\DependencyInjection\Attribute\Target;
@@ -251,7 +252,12 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
251252
}
252253

253254
foreach ($config['voice'] as $voiceProvider => $provider) {
254-
$this->processVoiceProviderConfig($voiceProvider, $provider, $builder);
255+
$this->processSpeechConfig($voiceProvider, $provider, $builder);
256+
}
257+
258+
$speechProviders = array_keys($builder->findTaggedServiceIds('ai.speech_provider'));
259+
if ([] === $speechProviders) {
260+
$builder->removeDefinition('ai.speech_provider.listener');
255261
}
256262

257263
foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
@@ -420,11 +426,9 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
420426
}
421427

422428
if ('eleven_labs' === $type) {
423-
$platformId = 'ai.platform.eleven_labs';
424429
$definition = (new Definition(Platform::class))
425430
->setFactory(ElevenLabsPlatformFactory::class.'::create')
426431
->setLazy(true)
427-
->addTag('proxy', ['interface' => PlatformInterface::class])
428432
->setArguments([
429433
$platform['api_key'],
430434
$platform['host'],
@@ -433,9 +437,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
433437
null,
434438
new Reference('event_dispatcher'),
435439
])
440+
->addTag('proxy', ['interface' => PlatformInterface::class])
436441
->addTag('ai.platform', ['name' => 'eleven_labs']);
437442

438-
$container->setDefinition($platformId, $definition);
443+
$container->setDefinition('ai.platform.eleven_labs', $definition);
439444

440445
return;
441446
}
@@ -1770,23 +1775,33 @@ private function processChatConfig(string $name, array $configuration, Container
17701775
/**
17711776
* @param array<string, mixed> $providers
17721777
*/
1773-
private function processVoiceProviderConfig(string $name, array $providers, ContainerBuilder $container): void
1778+
private function processSpeechConfig(string $name, array $providers, ContainerBuilder $container): void
17741779
{
17751780
if ('eleven_labs' === $name) {
1776-
foreach ($providers as $type => $config) {
1781+
foreach ($providers as $config) {
1782+
$configurationDefinition = new Definition(SpeechConfiguration::class);
1783+
$configurationDefinition
1784+
->setLazy(true)
1785+
->setArguments([
1786+
$config['tts_model'],
1787+
$config['tts_voice'],
1788+
$config['stt_model'],
1789+
]);
1790+
1791+
$container->setDefinition('ai.speech.eleven_labs.configuration', $configurationDefinition);
1792+
17771793
$definition = new Definition(ElevenLabsSpeechProvider::class);
17781794
$definition
17791795
->setLazy(true)
17801796
->setArguments([
17811797
new Reference('ai.platform.eleven_labs'),
1782-
$config['model'],
1798+
new Reference('ai.speech.eleven_labs.configuration'),
17831799
])
1784-
->addTag('proxy', ['interface' => VoiceProviderInterface::class])
1785-
->addTag('ai.voice.provider');
1800+
->addTag('proxy', ['interface' => SpeechProviderInterface::class])
1801+
->addTag('kernel.event_subscriber')
1802+
->addTag('ai.speech_provider');
17861803

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);
1804+
$container->setDefinition('ai.speech.eleven_labs.'.$name, $definition);
17901805
}
17911806
}
17921807
}

src/platform/src/Bridge/ElevenLabs/ElevenLabsSpeechProvider.php

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,38 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\AI\Voice\Bridge\ElevenLabs;
12+
namespace Symfony\AI\Platform\Bridge\ElevenLabs;
1313

14-
use Symfony\AI\Agent\Output;
14+
use Symfony\AI\Platform\Platform;
15+
use Symfony\AI\Platform\Result\DeferredResult;
1516
use Symfony\AI\Platform\Speech\Speech;
16-
use Symfony\AI\Platform\Speech\SpeechListenerInterface;
17+
use Symfony\AI\Platform\Speech\SpeechConfiguration;
1718
use Symfony\AI\Platform\Speech\SpeechProviderInterface;
18-
use Symfony\AI\Platform\Platform;
1919

2020
/**
2121
* @author Guillaume Loulier <[email protected]>
2222
*/
23-
final class ElevenLabsSpeechProvider implements SpeechProviderInterface, SpeechListenerInterface
23+
final class ElevenLabsSpeechProvider implements SpeechProviderInterface
2424
{
25-
public const ELEVEN_LABS_STT_MODEL = 'eleven_labs.stt_model';
25+
public const ELEVEN_LABS_STT_MODEL = 'eleven_labs.enable_tts';
2626

2727
public function __construct(
2828
private readonly Platform $platform,
29-
private readonly string $model,
29+
private readonly SpeechConfiguration $speechConfiguration,
3030
) {
3131
}
3232

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
33+
public function addSpeech(DeferredResult $result, array $options): void
4334
{
44-
$model = $options[self::ELEVEN_LABS_STT_MODEL];
45-
4635
unset($options[self::ELEVEN_LABS_STT_MODEL]);
4736

48-
$text = $this->platform->invoke($model, $input);
37+
$speechResult = $this->platform->invoke($this->speechConfiguration->ttsModel, $result->asText(), $options);
4938

50-
return new Speech($input, $text);
39+
$result->setSpeech(new Speech($result->asText(), $speechResult));
5140
}
5241

53-
public function supportListening(object|array|string $input, array $options): bool
42+
public function support(DeferredResult $result, array $options): bool
5443
{
55-
return \array_key_exists(self::ELEVEN_LABS_STT_MODEL, $options);
44+
return $options[self::ELEVEN_LABS_STT_MODEL] ?? false;
5645
}
5746
}

src/platform/src/Platform.php

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
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;
2221

2322
/**
2423
* @author Christopher Hertel <[email protected]>
@@ -35,28 +34,20 @@ final class Platform implements PlatformInterface
3534
*/
3635
private readonly array $resultConverters;
3736

38-
/**
39-
* @var SpeechListenerInterface[]
40-
*/
41-
private readonly iterable $speechListeners;
42-
4337
/**
4438
* @param iterable<ModelClientInterface> $modelClients
4539
* @param iterable<ResultConverterInterface> $resultConverters
46-
* @param iterable<SpeechListenerInterface> $speechListeners
4740
*/
4841
public function __construct(
4942
iterable $modelClients,
5043
iterable $resultConverters,
51-
iterable $speechListeners,
5244
private readonly ModelCatalogInterface $modelCatalog,
5345
private ?Contract $contract = null,
5446
private readonly ?EventDispatcherInterface $eventDispatcher = null,
5547
) {
5648
$this->contract = $contract ?? Contract::create();
5749
$this->modelClients = $modelClients instanceof \Traversable ? iterator_to_array($modelClients) : $modelClients;
5850
$this->resultConverters = $resultConverters instanceof \Traversable ? iterator_to_array($resultConverters) : $resultConverters;
59-
$this->speechListeners = $speechListeners instanceof \Traversable ? iterator_to_array($speechListeners) : $speechListeners;
6051
}
6152

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

7667
$result = $this->convertResult($model, $this->doInvoke($model, $payload, $options), $options);
77-
$finalResult = $this->addSpeech($result, $payload, $options);
7868

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

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

116106
throw new RuntimeException(\sprintf('No ResultConverter registered for model "%s" with given input.', $model::class));
117107
}
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-
}
135108
}

src/platform/src/Result/BaseResult.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\AI\Platform\Result;
1313

1414
use Symfony\AI\Platform\Metadata\MetadataAwareTrait;
15+
use Symfony\AI\Platform\Speech\SpeechAwareTrait;
1516

1617
/**
1718
* Base result of converted result classes.
@@ -22,4 +23,5 @@ abstract class BaseResult implements ResultInterface
2223
{
2324
use MetadataAwareTrait;
2425
use RawResultAwareTrait;
26+
use SpeechAwareTrait;
2527
}

src/platform/src/Result/DeferredResult.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
use Symfony\AI\Platform\Exception\UnexpectedResultTypeException;
1616
use Symfony\AI\Platform\Metadata\MetadataAwareTrait;
1717
use Symfony\AI\Platform\ResultConverterInterface;
18-
use Symfony\AI\Platform\Vector\Vector;
1918
use Symfony\AI\Platform\Speech\SpeechAwareTrait;
19+
use Symfony\AI\Platform\Vector\Vector;
2020

2121
/**
2222
* @author Christopher Hertel <[email protected]>

src/platform/src/Result/ResultInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ public function getRawResult(): ?RawResultInterface;
3333
* @throws RawResultAlreadySetException if the result is tried to be set more than once
3434
*/
3535
public function setRawResult(RawResultInterface $rawResult): void;
36+
37+
public function getSpeech(): string;
3638
}

src/platform/src/Speech/SpeechAwareTrait.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public function setSpeech(Speech $speech): void
2323
$this->speech = $speech;
2424
}
2525

26-
public function getSpeech(): Speech
26+
public function getSpeech(): string
2727
{
28-
return $this->speech;
28+
return $this->speech->result->asBinary();
2929
}
3030
}

0 commit comments

Comments
 (0)