Skip to content

Commit

Permalink
feat: audio support
Browse files Browse the repository at this point in the history
  • Loading branch information
chr-hertel committed Jan 5, 2025
1 parent 4adb1a2 commit 9de3b8e
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 3 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,3 +601,10 @@ Contributions are always welcome, so feel free to join the development of this l
[![LLM Chain Contributors](https://contrib.rocks/image?repo=php-llm/llm-chain 'LLM Chain Contributors')](https://github.com/php-llm/llm-chain/graphs/contributors)

Made with [contrib.rocks](https://contrib.rocks).

### Fixture Licenses

For testing multi-modal features, the repository contains binary media content, with the following owners and licenses:

* `tests/Fixture/image.jpg`: Chris F., Creative Commons, see [pexels.com](https://www.pexels.com/photo/blauer-und-gruner-elefant-mit-licht-1680755/)
* `tests/Fixture/audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/)
31 changes: 31 additions & 0 deletions examples/audio-describer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use PhpLlm\LlmChain\Bridge\OpenAI\GPT;
use PhpLlm\LlmChain\Bridge\OpenAI\PlatformFactory;
use PhpLlm\LlmChain\Chain;
use PhpLlm\LlmChain\Model\Message\Content\Audio;
use PhpLlm\LlmChain\Model\Message\Message;
use PhpLlm\LlmChain\Model\Message\MessageBag;
use Symfony\Component\Dotenv\Dotenv;

require_once dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');

if (empty($_ENV['OPENAI_API_KEY'])) {
echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL;
exit(1);
}

$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
$llm = new GPT(GPT::GPT_4O_AUDIO);

$chain = new Chain($platform, $llm);
$messages = new MessageBag(
Message::ofUser(
'What is this recording about?',
new Audio(dirname(__DIR__).'/tests/Fixture/audio.mp3'),
),
);
$response = $chain->call($messages);

echo $response->getContent().PHP_EOL;
2 changes: 1 addition & 1 deletion examples/image-describer-binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
Message::forSystem('You are an image analyzer bot that helps identify the content of images.'),
Message::ofUser(
'Describe the image as a comedian would do it.',
new Image(dirname(__DIR__).'/tests/Fixture/image.png'),
new Image(dirname(__DIR__).'/tests/Fixture/image.jpg'),
),
);
$response = $chain->call($messages);
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/OpenAI/GPT.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class GPT implements LanguageModel
public const GPT_4_TURBO = 'gpt-4-turbo';
public const GPT_4O = 'gpt-4o';
public const GPT_4O_MINI = 'gpt-4o-mini';
public const GPT_4O_AUDIO = 'gpt-4o-audio-preview';
public const O1_MINI = 'o1-mini';
public const O1_PREVIEW = 'o1-preview';

Expand Down
35 changes: 35 additions & 0 deletions src/Model/Message/Content/Audio.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Model\Message\Content;

use PhpLlm\LlmChain\Exception\InvalidArgumentException;

final readonly class Audio implements Content
{
public function __construct(
public string $path,
) {
if (!is_readable($path) || false === file_get_contents($path)) {
throw new InvalidArgumentException(sprintf('The file "%s" does not exist or is not readable.', $path));
}
}

/**
* @return array{type: 'input_audio', input_audio: array{data: string, format: string}}
*/
public function jsonSerialize(): array
{
$data = file_get_contents($this->path);
$format = pathinfo($this->path, PATHINFO_EXTENSION);

return [
'type' => 'input_audio',
'input_audio' => [
'data' => base64_encode($data),
'format' => $format,
],
];
}
}
Binary file added tests/Fixture/audio.mp3
Binary file not shown.
Binary file added tests/Fixture/image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed tests/Fixture/image.png
Binary file not shown.
60 changes: 60 additions & 0 deletions tests/Model/Message/Content/AudioTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Model\Message\Content;

use PhpLlm\LlmChain\Model\Message\Content\Audio;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[CoversClass(Audio::class)]
#[Small]
final class AudioTest extends TestCase
{
#[Test]
public function constructWithValidPath(): void
{
$audio = new Audio(dirname(__DIR__, 3).'/Fixture/audio.mp3');

self::assertSame(dirname(__DIR__, 3).'/Fixture/audio.mp3', $audio->path);
}

#[Test]
#[DataProvider('provideValidPaths')]
public function jsonSerializeWithValid(string $path, array $expected): void
{
$audio = new Audio($path);

$expected = [
'type' => 'input_audio',
'input_audio' => $expected,
];

$actual = $audio->jsonSerialize();

// shortening the base64 data
$actual['input_audio']['data'] = substr($actual['input_audio']['data'], 0, 30);

self::assertSame($expected, $actual);
}

public static function provideValidPaths(): \Generator
{
yield 'mp3' => [dirname(__DIR__, 3).'/Fixture/audio.mp3', [
'data' => 'SUQzBAAAAAAAfVREUkMAAAAMAAADMj', // shortened
'format' => 'mp3',
]];
}

#[Test]
public function constructWithInvalidPath(): void
{
$this->expectExceptionMessage('The file "foo.mp3" does not exist or is not readable.');

new Audio('foo.mp3');
}
}
4 changes: 2 additions & 2 deletions tests/Model/Message/Content/ImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public function constructWithValidDataUrl(): void
#[Test]
public function withValidFile(): void
{
$image = new Image(dirname(__DIR__, 3).'/Fixture/image.png');
$image = new Image(dirname(__DIR__, 3).'/Fixture/image.jpg');

self::assertStringStartsWith('data:image/png;base64,', $image->url);
self::assertStringStartsWith('data:image/jpg;base64,', $image->url);
}

#[Test]
Expand Down

0 comments on commit 9de3b8e

Please sign in to comment.