Skip to content
Open
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
42 changes: 40 additions & 2 deletions src/Audio/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ class PendingRequest
use ConfiguresProviders;
use HasProviderOptions;

protected string|Audio $input;
protected string|Audio|int $input;

protected string $voice;

public function withInput(string|Audio $input): self
public function withInput(string|Audio|int $input): self
{
$this->input = $input;

Expand Down Expand Up @@ -59,6 +59,28 @@ public function asText(): TextResponse
}
}

public function asTextProviderId(): ProviderIdResponse
{
$request = $this->toSpeechToTextRequest();

try {
return $this->provider->speechToTextProviderId($request);
} catch (RequestException $e) {
$this->provider->handleRequestException($request->model(), $e);
}
}

public function asTextAsync(): TextResponse
{
$request = $this->toSpeechToTextAsyncRequest();

try {
return $this->provider->speechToTextAsync($request);
} catch (RequestException $e) {
$this->provider->handleRequestException($request->model(), $e);
}
}

protected function toTextToSpeechRequest(): TextToSpeechRequest
{
if (! is_string($this->input)) {
Expand Down Expand Up @@ -91,4 +113,20 @@ protected function toSpeechToTextRequest(): SpeechToTextRequest
providerOptions: $this->providerOptions,
);
}

protected function toSpeechToTextAsyncRequest(): SpeechToTextAsyncRequest
{
if (! is_string($this->input) && ! is_int($this->input)) {
throw new InvalidArgumentException('Async speech-to-text requires the input be the Provider ID as a string or integer');
}

return new SpeechToTextAsyncRequest(
model: $this->model,
providerKey: $this->providerKey(),
input: $this->input,
clientOptions: $this->clientOptions,
clientRetry: $this->clientRetry,
providerOptions: $this->providerOptions,
);
}
}
17 changes: 17 additions & 0 deletions src/Audio/ProviderIdResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Audio;

use Prism\Prism\ValueObjects\Usage;

readonly class ProviderIdResponse
{
public function __construct(
public string|int $id,
public ?Usage $usage = null,
/** @var array<string,mixed> */
public array $additionalContent = []
) {}
}
62 changes: 62 additions & 0 deletions src/Audio/SpeechToTextAsyncRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Audio;

use Closure;
use Prism\Prism\Concerns\ChecksSelf;
use Prism\Prism\Concerns\HasProviderOptions;
use Prism\Prism\Contracts\PrismRequest;

class SpeechToTextAsyncRequest implements PrismRequest
{
use ChecksSelf, HasProviderOptions;

/**
* @param array<string, mixed> $clientOptions
* @param array{0: array<int, int>|int, 1?: Closure|int, 2?: ?callable, 3?: bool} $clientRetry
* @param array<string, mixed> $providerOptions
*/
public function __construct(
protected string $model,
protected string $providerKey,
protected string|int $input,
protected array $clientOptions,
protected array $clientRetry,
array $providerOptions = [],
) {
$this->providerOptions = $providerOptions;
}

/**
* @return array{0: array<int, int>|int, 1?: Closure|int, 2?: ?callable, 3?: bool}
*/
public function clientRetry(): array
{
return $this->clientRetry;
}

/**
* @return array<string, mixed>
*/
public function clientOptions(): array
{
return $this->clientOptions;
}

public function input(): string|int
{
return $this->input;
}

public function model(): string
{
return $this->model;
}

public function provider(): string
{
return $this->providerKey;
}
}
12 changes: 12 additions & 0 deletions src/Providers/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Generator;
use Illuminate\Http\Client\RequestException;
use Prism\Prism\Audio\AudioResponse as TextToSpeechResponse;
use Prism\Prism\Audio\ProviderIdResponse;
use Prism\Prism\Audio\SpeechToTextAsyncRequest;
use Prism\Prism\Audio\SpeechToTextRequest;
use Prism\Prism\Audio\TextResponse as SpeechToTextResponse;
use Prism\Prism\Audio\TextToSpeechRequest;
Expand Down Expand Up @@ -56,6 +58,16 @@ public function speechToText(SpeechToTextRequest $request): SpeechToTextResponse
throw PrismException::unsupportedProviderAction('speechToText', class_basename($this));
}

public function speechToTextProviderId(SpeechToTextRequest $request): ProviderIdResponse
{
throw PrismException::unsupportedProviderAction('speechToTextProviderId', class_basename($this));
}

public function speechToTextAsync(SpeechToTextAsyncRequest $request): SpeechToTextResponse
{
throw PrismException::unsupportedProviderAction('speechToTextAsync', class_basename($this));
}

/**
* @return Generator<StreamEvent>
*/
Expand Down
52 changes: 52 additions & 0 deletions tests/Audio/PendingRequestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

use Prism\Prism\Audio\PendingRequest;
use Prism\Prism\Audio\ProviderIdResponse;
use Prism\Prism\Audio\SpeechToTextAsyncRequest;
use Prism\Prism\Audio\SpeechToTextRequest;
use Prism\Prism\Providers\Provider as ProviderContract;
use Prism\Prism\ValueObjects\Media\Audio;
use Tests\TestDoubles\TestProvider;

beforeEach(function (): void {
$this->pendingRequest = new PendingRequest;
});

test('it generates a provider id response for speech to text', function (): void {
resolve('prism-manager')->extend('test-provider', fn ($config): ProviderContract => new TestProvider);

$audio = Audio::fromUrl('https://example.com/audio.mp3', 'audio/mpeg');

$response = $this->pendingRequest
->using('test-provider', 'test-model')
->withInput($audio)
->asTextProviderId();

$provider = $this->pendingRequest->provider();

expect($response)
->toBeInstanceOf(ProviderIdResponse::class)
->and($response->id)->toBe('provider-id')
->and($provider->request)->toBeInstanceOf(SpeechToTextRequest::class)
->and($provider->request->input())->toBe($audio);
});

test('it generates a response for async speech to text', function (): void {
resolve('prism-manager')->extend('test-provider', fn ($config): ProviderContract => new TestProvider);

$providerId = 'provider-id-123';

$response = $this->pendingRequest
->using('test-provider', 'test-model')
->withInput($providerId)
->asTextAsync();

$provider = $this->pendingRequest->provider();

expect($response->text)->toBe('Async transcript')
->and($provider->request)->toBeInstanceOf(SpeechToTextAsyncRequest::class)
->and($provider->request->model())->toBe('test-model')
->and($provider->request->input())->toBe($providerId);
});
30 changes: 27 additions & 3 deletions tests/TestDoubles/TestProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
namespace Tests\TestDoubles;

use Generator;
use Prism\Prism\Audio\ProviderIdResponse;
use Prism\Prism\Audio\SpeechToTextAsyncRequest;
use Prism\Prism\Audio\SpeechToTextRequest;
use Prism\Prism\Audio\TextResponse as AudioTextResponse;
use Prism\Prism\Embeddings\Request as EmbeddingRequest;
use Prism\Prism\Embeddings\Response as EmbeddingResponse;
use Prism\Prism\Enums\FinishReason;
Expand All @@ -24,15 +28,15 @@

class TestProvider extends Provider
{
public StructuredRequest|TextRequest|EmbeddingRequest|ImageRequest $request;
public StructuredRequest|TextRequest|EmbeddingRequest|ImageRequest|SpeechToTextRequest|SpeechToTextAsyncRequest $request;

/** @var array<string, mixed> */
public array $clientOptions;

/** @var array<mixed> */
public array $clientRetry;

/** @var array<int, StructuredResponse|TextResponse|EmbeddingResponse|ImageResponse> */
/** @var array<int, StructuredResponse|TextResponse|EmbeddingResponse|ImageResponse|AudioTextResponse|ProviderIdResponse> */
public array $responses = [];

public $callCount = 0;
Expand Down Expand Up @@ -115,7 +119,27 @@ public function stream(TextRequest $request): Generator
throw PrismException::unsupportedProviderAction(__METHOD__, class_basename($this));
}

public function withResponse(StructuredResponse|TextResponse $response): Provider
#[\Override]
public function speechToTextProviderId(SpeechToTextRequest $request): ProviderIdResponse
{
$this->callCount++;

$this->request = $request;

return $this->responses[$this->callCount - 1] ?? new ProviderIdResponse('provider-id');
}

#[\Override]
public function speechToTextAsync(SpeechToTextAsyncRequest $request): AudioTextResponse
{
$this->callCount++;

$this->request = $request;

return $this->responses[$this->callCount - 1] ?? new AudioTextResponse('Async transcript');
}

public function withResponse(StructuredResponse|TextResponse|EmbeddingResponse|ImageResponse|AudioTextResponse|ProviderIdResponse $response): Provider
{
$this->responses[] = $response;

Expand Down