Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/components/platform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,9 @@ This allows fast and isolated testing of AI-powered features without relying on

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

Adding Voice
~~~~~~~~~~~~

Code Examples
~~~~~~~~~~~~~

Expand Down
44 changes: 44 additions & 0 deletions examples/voice/agent-eleven-labs-voice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Platform\Bridge\ElevenLabs\ElevenLabsSpeechProvider;
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Speech\SpeechConfiguration;
use Symfony\AI\Platform\Speech\SpeechProviderListener;
use Symfony\Component\EventDispatcher\EventDispatcher;

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

$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber(new SpeechProviderListener([
new ElevenLabsSpeechProvider(PlatformFactory::create(
apiKey: env('ELEVEN_LABS_API_KEY'),
httpClient: http_client(),
), new SpeechConfiguration(
'eleven_multilingual_v2',
'Dslrhjl3ZpzrctukrQSN', // Brad (https://elevenlabs.io/app/voice-library?voiceId=Dslrhjl3ZpzrctukrQSN)
'eleven_multilingual_v2'
)),
]));

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

$agent = new Agent($platform, 'gpt-4o');
$answer = $agent->call(new MessageBag(
Message::ofUser('Hello'),
), [
ElevenLabsSpeechProvider::ELEVEN_LABS_STT_MODEL => true,
]);

echo $answer->getSpeech();
4 changes: 2 additions & 2 deletions src/agent/src/Agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function getName(): string
public function call(MessageBag $messages, array $options = []): ResultInterface
{
$input = new Input($this->getModel(), $messages, $options);
array_map(fn (InputProcessorInterface $processor) => $processor->processInput($input), $this->inputProcessors);
array_map(static fn (InputProcessorInterface $processor) => $processor->processInput($input), $this->inputProcessors);

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

$output = new Output($model, $result, $messages, $options);
array_map(fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors);
array_map(static fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors);

return $output->getResult();
}
Expand Down
12 changes: 12 additions & 0 deletions src/agent/src/Output.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Result\ResultInterface;
use Symfony\AI\Platform\Speech\Speech;

/**
* @author Christopher Hertel <[email protected]>
Expand All @@ -27,6 +28,7 @@ public function __construct(
private ResultInterface $result,
private readonly MessageBag $messageBag,
private readonly array $options = [],
private ?Speech $speech = null,
) {
}

Expand Down Expand Up @@ -57,4 +59,14 @@ public function getOptions(): array
{
return $this->options;
}

public function setSpeech(?Speech $speech): void
{
$this->speech = $speech;
}

public function getSpeech(): ?Speech
{
return $this->speech;
}
}
12 changes: 12 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,18 @@
->end()
->end()
->end()
->arrayNode('voice')
->children()
->arrayNode('eleven_labs')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->stringNode('model')->cannotBeEmpty()->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('vectorizer')
->info('Vectorizers for converting strings to Vector objects and transforming TextDocument arrays to VectorDocument arrays')
->useAttributeAsKey('name')
Expand Down
56 changes: 51 additions & 5 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory as DeepSeekPlatformFactory;
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory as DockerModelRunnerPlatformFactory;
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsSpeechProvider;
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory as GeminiPlatformFactory;
use Symfony\AI\Platform\Bridge\HuggingFace\PlatformFactory as HuggingFacePlatformFactory;
Expand All @@ -75,6 +76,8 @@
use Symfony\AI\Platform\Platform;
use Symfony\AI\Platform\PlatformInterface;
use Symfony\AI\Platform\ResultConverterInterface;
use Symfony\AI\Platform\Speech\SpeechConfiguration;
use Symfony\AI\Platform\Speech\SpeechProviderInterface;
use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureSearchStore;
use Symfony\AI\Store\Bridge\ChromaDb\Store as ChromaDbStore;
use Symfony\AI\Store\Bridge\ClickHouse\Store as ClickHouseStore;
Expand Down Expand Up @@ -248,6 +251,15 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
}
}

foreach ($config['voice'] as $voiceProvider => $provider) {
$this->processSpeechConfig($voiceProvider, $provider, $builder);
}

$speechProviders = array_keys($builder->findTaggedServiceIds('ai.speech_provider'));
if ([] === $speechProviders) {
$builder->removeDefinition('ai.speech_provider.listener');
}

foreach ($config['vectorizer'] ?? [] as $vectorizerName => $vectorizer) {
$this->processVectorizerConfig($vectorizerName, $vectorizer, $builder);
}
Expand Down Expand Up @@ -414,11 +426,9 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
}

if ('eleven_labs' === $type) {
$platformId = 'ai.platform.eleven_labs';
$definition = (new Definition(Platform::class))
->setFactory(ElevenLabsPlatformFactory::class.'::create')
->setLazy(true)
->addTag('proxy', ['interface' => PlatformInterface::class])
->setArguments([
$platform['api_key'],
$platform['host'],
Expand All @@ -427,9 +437,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
null,
new Reference('event_dispatcher'),
])
->addTag('proxy', ['interface' => PlatformInterface::class])
->addTag('ai.platform', ['name' => 'eleven_labs']);

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

return;
}
Expand Down Expand Up @@ -917,8 +928,9 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde
$agentDefinition
->setArgument(2, []) // placeholder until ProcessorCompilerPass process.
->setArgument(3, []) // placeholder until ProcessorCompilerPass process.
->setArgument(4, $name)
->setArgument(5, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
->setArgument(4, []) // placeholder until VoiceProviderCompilerPass process.
->setArgument(5, $name)
->setArgument(6, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))
;

$container->setDefinition($agentId, $agentDefinition);
Expand Down Expand Up @@ -1760,6 +1772,40 @@ private function processChatConfig(string $name, array $configuration, Container
$container->registerAliasForArgument('ai.chat.'.$name, ChatInterface::class, $name);
}

/**
* @param array<string, mixed> $providers
*/
private function processSpeechConfig(string $name, array $providers, ContainerBuilder $container): void
{
if ('eleven_labs' === $name) {
foreach ($providers as $config) {
$configurationDefinition = new Definition(SpeechConfiguration::class);
$configurationDefinition
->setLazy(true)
->setArguments([
$config['tts_model'],
$config['tts_voice'],
$config['stt_model'],
]);

$container->setDefinition('ai.speech.eleven_labs.configuration', $configurationDefinition);

$definition = new Definition(ElevenLabsSpeechProvider::class);
$definition
->setLazy(true)
->setArguments([
new Reference('ai.platform.eleven_labs'),
new Reference('ai.speech.eleven_labs.configuration'),
])
->addTag('proxy', ['interface' => SpeechProviderInterface::class])
->addTag('kernel.event_subscriber')
->addTag('ai.speech_provider');

$container->setDefinition('ai.speech.eleven_labs.'.$name, $definition);
}
}
}

/**
* @param array<string, mixed> $config
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\AiBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* @author Guillaume Loulier <[email protected]>
*/
final class VoiceProviderCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$voiceProviders = $container->findTaggedServiceIds('ai.voice.provider');
}
}
46 changes: 46 additions & 0 deletions src/platform/src/Bridge/ElevenLabs/ElevenLabsSpeechProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\ElevenLabs;

use Symfony\AI\Platform\Platform;
use Symfony\AI\Platform\Result\DeferredResult;
use Symfony\AI\Platform\Speech\Speech;
use Symfony\AI\Platform\Speech\SpeechConfiguration;
use Symfony\AI\Platform\Speech\SpeechProviderInterface;

/**
* @author Guillaume Loulier <[email protected]>
*/
final class ElevenLabsSpeechProvider implements SpeechProviderInterface
{
public const ELEVEN_LABS_STT_MODEL = 'eleven_labs.enable_tts';

public function __construct(
private readonly Platform $platform,
private readonly SpeechConfiguration $speechConfiguration,
) {
}

public function addSpeech(DeferredResult $result, array $options): void
{
unset($options[self::ELEVEN_LABS_STT_MODEL]);

$speechResult = $this->platform->invoke($this->speechConfiguration->ttsModel, $result->asText(), $options);

$result->setSpeech(new Speech($result->asText(), $speechResult));
}

public function support(DeferredResult $result, array $options): bool
{
return $options[self::ELEVEN_LABS_STT_MODEL] ?? false;
}
}
2 changes: 2 additions & 0 deletions src/platform/src/Result/BaseResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\AI\Platform\Result;

use Symfony\AI\Platform\Metadata\MetadataAwareTrait;
use Symfony\AI\Platform\Speech\SpeechAwareTrait;

/**
* Base result of converted result classes.
Expand All @@ -22,4 +23,5 @@ abstract class BaseResult implements ResultInterface
{
use MetadataAwareTrait;
use RawResultAwareTrait;
use SpeechAwareTrait;
}
10 changes: 10 additions & 0 deletions src/platform/src/Result/DeferredResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\AI\Platform\Exception\UnexpectedResultTypeException;
use Symfony\AI\Platform\Metadata\MetadataAwareTrait;
use Symfony\AI\Platform\ResultConverterInterface;
use Symfony\AI\Platform\Speech\SpeechAwareTrait;
use Symfony\AI\Platform\Vector\Vector;

/**
Expand All @@ -23,6 +24,7 @@
final class DeferredResult
{
use MetadataAwareTrait;
use SpeechAwareTrait;

private bool $isConverted = false;
private ResultInterface $convertedResult;
Expand Down Expand Up @@ -132,6 +134,14 @@ public function asToolCalls(): array
return $this->as(ToolCallResult::class)->getContent();
}

/**
* @throws ExceptionInterface
*/
public function asVoice(): string
{
return $this->as(VoiceResult::class)->getContent();
}

/**
* @param class-string $type
*
Expand Down
2 changes: 2 additions & 0 deletions src/platform/src/Result/ResultInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ public function getRawResult(): ?RawResultInterface;
* @throws RawResultAlreadySetException if the result is tried to be set more than once
*/
public function setRawResult(RawResultInterface $rawResult): void;

public function getSpeech(): string;
}
26 changes: 26 additions & 0 deletions src/platform/src/Speech/Speech.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Speech;

use Symfony\AI\Platform\Result\DeferredResult;

/**
* @author Guillaume Loulier <[email protected]>
*/
final class Speech
{
public function __construct(
public readonly string|array $payload,
public DeferredResult $result,
) {
}
}
Loading
Loading