Skip to content

Commit

Permalink
Add stream support for Text To Speech
Browse files Browse the repository at this point in the history
  • Loading branch information
gehrisandro committed Dec 5, 2023
1 parent 44ac258 commit 69a19ba
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 6 deletions.
10 changes: 10 additions & 0 deletions src/Contracts/Resources/AudioContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace OpenAI\Contracts\Resources;

use OpenAI\Responses\Audio\SpeechStreamResponse;
use OpenAI\Responses\Audio\TranscriptionResponse;
use OpenAI\Responses\Audio\TranslationResponse;

Expand All @@ -16,6 +17,15 @@ interface AudioContract
*/
public function speech(array $parameters): string;

/**
* Generates streamed audio from the input text.
*
* @see https://platform.openai.com/docs/api-reference/audio/createSpeech
*
* @param array<string, mixed> $parameters
*/
public function speechStreamed(array $parameters): SpeechStreamResponse;

/**
* Transcribes audio into the input language.
*
Expand Down
18 changes: 18 additions & 0 deletions src/Contracts/ResponseStreamContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace OpenAI\Contracts;

use IteratorAggregate;

/**
* @template T
*
* @extends IteratorAggregate<int, T>
*
* @internal
*/
interface ResponseStreamContract extends IteratorAggregate
{
}
17 changes: 17 additions & 0 deletions src/Resources/Audio.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace OpenAI\Resources;

use OpenAI\Contracts\Resources\AudioContract;
use OpenAI\Responses\Audio\SpeechStreamResponse;
use OpenAI\Responses\Audio\TranscriptionResponse;
use OpenAI\Responses\Audio\TranslationResponse;
use OpenAI\ValueObjects\Transporter\Payload;
Expand All @@ -28,6 +29,22 @@ public function speech(array $parameters): string
return $this->transporter->requestContent($payload);
}

/**
* Generates streamed audio from the input text.
*
* @see https://platform.openai.com/docs/api-reference/audio/createSpeech
*
* @param array<string, mixed> $parameters
*/
public function speechStreamed(array $parameters): SpeechStreamResponse
{
$payload = Payload::create('audio/speech', $parameters);

$response = $this->transporter->requestStream($payload);

return new SpeechStreamResponse($response);
}

/**
* Transcribes audio into the input language.
*
Expand Down
53 changes: 53 additions & 0 deletions src/Responses/Audio/SpeechStreamResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace OpenAI\Responses\Audio;

use Generator;
use Http\Discovery\Psr17Factory;
use OpenAI\Contracts\ResponseHasMetaInformationContract;
use OpenAI\Contracts\ResponseStreamContract;
use OpenAI\Responses\Meta\MetaInformation;
use Psr\Http\Message\ResponseInterface;

/**
* @implements ResponseStreamContract<string>
*/
final class SpeechStreamResponse implements ResponseHasMetaInformationContract, ResponseStreamContract
{
public function __construct(
private readonly ResponseInterface $response,
) {
//
}

/**
* {@inheritDoc}
*/
public function getIterator(): Generator
{
while (! $this->response->getBody()->eof()) {
yield $this->response->getBody()->read(1024);
}
}

public function meta(): MetaInformation
{
// @phpstan-ignore-next-line
return MetaInformation::from($this->response->getHeaders());
}

public static function fake(string $content = null, MetaInformation $meta = null): static
{
$psr17Factory = new Psr17Factory();
$response = $psr17Factory->createResponse()
->withBody($psr17Factory->createStream($content ?? (string) file_get_contents(__DIR__.'/../../Testing/Responses/Fixtures/Audio/speech-streamed.mp3')));

if ($meta instanceof \OpenAI\Responses\Meta\MetaInformation) {
foreach ($meta->toArray() as $key => $value) {
$response = $response->withHeader($key, (string) $value);
}
}

return new self($response);
}
}
6 changes: 3 additions & 3 deletions src/Responses/StreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace OpenAI\Responses;

use Generator;
use IteratorAggregate;
use OpenAI\Contracts\ResponseHasMetaInformationContract;
use OpenAI\Contracts\ResponseStreamContract;
use OpenAI\Exceptions\ErrorException;
use OpenAI\Responses\Meta\MetaInformation;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -13,9 +13,9 @@
/**
* @template TResponse
*
* @implements IteratorAggregate<int, TResponse>
* @implements ResponseStreamContract<TResponse>
*/
final class StreamResponse implements IteratorAggregate, ResponseHasMetaInformationContract
final class StreamResponse implements ResponseHasMetaInformationContract, ResponseStreamContract
{
/**
* Creates a new Stream Response instance.
Expand Down
3 changes: 2 additions & 1 deletion src/Testing/ClientFake.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use OpenAI\Contracts\ClientContract;
use OpenAI\Contracts\ResponseContract;
use OpenAI\Contracts\ResponseStreamContract;
use OpenAI\Responses\StreamResponse;
use OpenAI\Testing\Requests\TestRequest;
use OpenAI\Testing\Resources\AssistantsTestResource;
Expand Down Expand Up @@ -113,7 +114,7 @@ private function resourcesOf(string $type): array
return array_filter($this->requests, fn (TestRequest $request): bool => $request->resource() === $type);
}

public function record(TestRequest $request): ResponseContract|StreamResponse|string
public function record(TestRequest $request): ResponseContract|ResponseStreamContract|string
{
$this->requests[] = $request;

Expand Down
6 changes: 6 additions & 0 deletions src/Testing/Resources/AudioTestResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use OpenAI\Contracts\Resources\AudioContract;
use OpenAI\Resources\Audio;
use OpenAI\Responses\Audio\SpeechStreamResponse;
use OpenAI\Responses\Audio\TranscriptionResponse;
use OpenAI\Responses\Audio\TranslationResponse;
use OpenAI\Testing\Resources\Concerns\Testable;
Expand All @@ -22,6 +23,11 @@ public function speech(array $parameters): string
return $this->record(__FUNCTION__, func_get_args());
}

public function speechStreamed(array $parameters): SpeechStreamResponse
{
return $this->record(__FUNCTION__, func_get_args());
}

public function transcribe(array $parameters): TranscriptionResponse
{
return $this->record(__FUNCTION__, func_get_args());
Expand Down
4 changes: 2 additions & 2 deletions src/Testing/Resources/Concerns/Testable.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace OpenAI\Testing\Resources\Concerns;

use OpenAI\Contracts\ResponseContract;
use OpenAI\Responses\StreamResponse;
use OpenAI\Contracts\ResponseStreamContract;
use OpenAI\Testing\ClientFake;
use OpenAI\Testing\Requests\TestRequest;

Expand All @@ -18,7 +18,7 @@ abstract protected function resource(): string;
/**
* @param array<string, mixed> $args
*/
protected function record(string $method, array $args = []): ResponseContract|StreamResponse|string
protected function record(string $method, array $args = []): ResponseContract|ResponseStreamContract|string
{
return $this->fake->record(new TestRequest($this->resource(), $method, $args));
}
Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions tests/Arch.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
]);

test('responses')->expect('OpenAI\Responses')->toOnlyUse([
'Http\Discovery\Psr17Factory',
'OpenAI\Enums',
'OpenAI\Exceptions\ErrorException',
'OpenAI\Contracts',
Expand Down
8 changes: 8 additions & 0 deletions tests/Fixtures/Audio.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,11 @@ function audioFileContent(): string
{
return file_get_contents(__DIR__.'/audio.mp3');
}

/**
* @return resource
*/
function speechStream()
{
return fopen(__DIR__.'/Streams/Speech.mp3', 'r');
}
Binary file added tests/Fixtures/Streams/Speech.mp3
Binary file not shown.
29 changes: 29 additions & 0 deletions tests/Resources/Audio.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;
use OpenAI\Responses\Audio\SpeechStreamResponse;
use OpenAI\Responses\Audio\TranscriptionResponse;
use OpenAI\Responses\Audio\TranscriptionResponseSegment;
use OpenAI\Responses\Audio\TranslationResponse;
Expand Down Expand Up @@ -209,3 +212,29 @@
->toBeString()
->toBe(audioFileContent());
});

test('text to speech streamed', function () {
$response = new Response(
body: new Stream(speechStream()),
headers: metaHeaders(),
);

$client = mockStreamClient('POST', 'audio/speech', [
'model' => 'tts-1',
'input' => 'Hello, how are you?',
'voice' => 'alloy',
], $response, 'requestContent');

$result = $client->audio()->speechStreamed([
'model' => 'tts-1',
'input' => 'Hello, how are you?',
'voice' => 'alloy',
]);

expect($result)
->toBeInstanceOf(SpeechStreamResponse::class)
->toBeInstanceOf(IteratorAggregate::class);

expect($result->getIterator())
->toBeInstanceOf(Iterator::class);
});
36 changes: 36 additions & 0 deletions tests/Responses/Audio/SpeechStreamResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;
use OpenAI\Responses\Audio\SpeechStreamResponse;
use OpenAI\Responses\Meta\MetaInformation;

test('from response', function () {
$response = new Response(
body: new Stream(speechStream()),
headers: metaHeaders(),
);

$speech = new SpeechStreamResponse($response);

expect($speech)
->toBeInstanceOf(SpeechStreamResponse::class)
->getIterator()->toBeInstanceOf(Iterator::class)
->meta()->toBeInstanceOf(MetaInformation::class);
});

test('fake', function () {
$response = SpeechStreamResponse::fake();

foreach ($response as $chunk) {
expect($chunk)->toBeString();
}
});

test('fake with override', function () {
$response = SpeechStreamResponse::fake('fake audio');

foreach ($response as $chunk) {
expect($chunk)->toBe('fake audio');
}
});
22 changes: 22 additions & 0 deletions tests/Testing/Resources/AudioTestResource.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use OpenAI\Resources\Audio;
use OpenAI\Responses\Audio\SpeechStreamResponse;
use OpenAI\Responses\Audio\TranscriptionResponse;
use OpenAI\Responses\Audio\TranslationResponse;
use OpenAI\Testing\ClientFake;
Expand All @@ -26,6 +27,27 @@
});
});

it('records a streamed speech request', function () {
$fake = new ClientFake([
SpeechStreamResponse::fake(),
]);

$fake->audio()->speechStreamed([
'model' => 'tts-1',
'input' => 'Hello, how are you?',
'voice' => 'alloy',
]);

$fake->assertSent(Audio::class, function ($method, $parameters) {
return $method === 'speechStreamed' &&
$parameters === [
'model' => 'tts-1',
'input' => 'Hello, how are you?',
'voice' => 'alloy',
];
});
});

it('records an audio transcription request', function () {
$fake = new ClientFake([
TranscriptionResponse::fake(),
Expand Down

0 comments on commit 69a19ba

Please sign in to comment.