Skip to content

Commit 897f995

Browse files
committed
implement audio translation endpoint - openai-php#59
1 parent f626a2e commit 897f995

File tree

8 files changed

+484
-0
lines changed

8 files changed

+484
-0
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,39 @@ foreach ($response->segments as $segment) {
207207
$response->toArray(); // ['task' => 'transcribe', ...]
208208
```
209209

210+
#### `translate`
211+
212+
Translates audio into English.
213+
214+
```php
215+
$response = $client->audio()->translate([
216+
'model' => 'whisper-1',
217+
'file' => fopen(storage_path('audio.mp3'), 'r'),
218+
'response_format' => 'verbose_json',
219+
]);
220+
221+
$response->task; // 'translate'
222+
$response->language; // 'english'
223+
$response->duration; // 2.95
224+
$reponse->text; // 'Hello, how are you?'
225+
226+
foreach ($response->segments as $segment) {
227+
$segment->index; // 0
228+
$segment->seek; // 0
229+
$segment->start; // 0.0
230+
$segment->end; // 4.0
231+
$segment->text; // 'Hello, how are you?'
232+
$segment->tokens; // [50364, 2425, 11, 577, 366, 291, 30, 50564]
233+
$segment->temperature; // 0.0
234+
$segment->avgLogprob; // -0.45045216878255206
235+
$segment->compressionRatio; // 0.7037037037037037
236+
$segment->noSpeechProb; // 0.1076972484588623
237+
$segment->transient; // false
238+
}
239+
240+
$response->toArray(); // ['task' => 'translate', ...]
241+
```
242+
210243
### `Edits` Resource
211244

212245
#### `create`

src/Resources/Audio.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace OpenAI\Resources;
66

77
use OpenAI\Responses\Audio\TranscriptionResponse;
8+
use OpenAI\Responses\Audio\TranslationResponse;
89
use OpenAI\ValueObjects\Transporter\Payload;
910

1011
final class Audio
@@ -27,4 +28,21 @@ public function transcribe(array $parameters): TranscriptionResponse
2728

2829
return TranscriptionResponse::from($result);
2930
}
31+
32+
/**
33+
* Translates audio into English.
34+
*
35+
* @see https://platform.openai.com/docs/api-reference/audio/create
36+
*
37+
* @param array<string, mixed> $parameters
38+
*/
39+
public function translate(array $parameters): TranslationResponse
40+
{
41+
$payload = Payload::upload('audio/translations', $parameters);
42+
43+
/** @var array{task: ?string, language: ?string, duration: ?float, segments: array<int, array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool}>, text: string}|string $result */
44+
$result = $this->transporter->requestObject($payload);
45+
46+
return TranslationResponse::from($result);
47+
}
3048
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Audio;
6+
7+
use OpenAI\Contracts\Response;
8+
use OpenAI\Responses\Concerns\ArrayAccessible;
9+
10+
/**
11+
* @implements Response<array{task: ?string, language: ?string, duration: ?float, segments: array<int, array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool}>, text: string}>
12+
*/
13+
final class TranslationResponse implements Response
14+
{
15+
/**
16+
* @use ArrayAccessible<array{task: ?string, language: ?string, duration: ?float, segments: array<int, array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool}>, text: string}>
17+
*/
18+
use ArrayAccessible;
19+
20+
/**
21+
* @param array<int, TranslationResponseSegment> $segments
22+
*/
23+
private function __construct(
24+
public readonly ?string $task,
25+
public readonly ?string $language,
26+
public readonly ?float $duration,
27+
public readonly array $segments,
28+
public readonly string $text,
29+
) {
30+
}
31+
32+
/**
33+
* Acts as static factory, and returns a new Response instance.
34+
*
35+
* @param array{task: ?string, language: ?string, duration: ?float, segments: array<int, array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool}>, text: string} $attributes
36+
*/
37+
public static function from(array|string $attributes): self
38+
{
39+
if (is_string($attributes)) {
40+
$attributes = ['text' => $attributes];
41+
}
42+
43+
$segments = isset($attributes['segments']) ? array_map(fn (array $result): TranslationResponseSegment => TranslationResponseSegment::from(
44+
$result
45+
), $attributes['segments']) : [];
46+
47+
return new self(
48+
$attributes['task'] ?? null,
49+
$attributes['language'] ?? null,
50+
$attributes['duration'] ?? null,
51+
$segments,
52+
$attributes['text'],
53+
);
54+
}
55+
56+
/**
57+
* {@inheritDoc}
58+
*/
59+
public function toArray(): array
60+
{
61+
return [
62+
'task' => $this->task,
63+
'language' => $this->language,
64+
'duration' => $this->duration,
65+
'segments' => array_map(
66+
static fn (TranslationResponseSegment $result): array => $result->toArray(),
67+
$this->segments,
68+
),
69+
'text' => $this->text,
70+
];
71+
}
72+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Audio;
6+
7+
use OpenAI\Contracts\Response;
8+
use OpenAI\Responses\Concerns\ArrayAccessible;
9+
10+
/**
11+
* @implements Response<array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool}>
12+
*/
13+
final class TranslationResponseSegment implements Response
14+
{
15+
/**
16+
* @use ArrayAccessible<array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool}>
17+
*/
18+
use ArrayAccessible;
19+
20+
/**
21+
* @param array<int, int> $tokens
22+
*/
23+
private function __construct(
24+
public readonly int $id,
25+
public readonly int $seek,
26+
public readonly float $start,
27+
public readonly float $end,
28+
public readonly string $text,
29+
public readonly array $tokens,
30+
public readonly float $temperature,
31+
public readonly float $avgLogprob,
32+
public readonly float $compressionRatio,
33+
public readonly float $noSpeechProb,
34+
public readonly bool $transient,
35+
) {
36+
}
37+
38+
/**
39+
* Acts as static factory, and returns a new Response instance.
40+
*
41+
* @param array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient: bool} $attributes
42+
*/
43+
public static function from(array $attributes): self
44+
{
45+
return new self(
46+
$attributes['id'],
47+
$attributes['seek'],
48+
$attributes['start'],
49+
$attributes['end'],
50+
$attributes['text'],
51+
$attributes['tokens'],
52+
$attributes['temperature'],
53+
$attributes['avg_logprob'],
54+
$attributes['compression_ratio'],
55+
$attributes['no_speech_prob'],
56+
$attributes['transient'],
57+
);
58+
}
59+
60+
/**
61+
* {@inheritDoc}
62+
*/
63+
public function toArray(): array
64+
{
65+
return [
66+
'id' => $this->id,
67+
'seek' => $this->seek,
68+
'start' => $this->start,
69+
'end' => $this->end,
70+
'text' => $this->text,
71+
'tokens' => $this->tokens,
72+
'temperature' => $this->temperature,
73+
'avg_logprob' => $this->avgLogprob,
74+
'compression_ratio' => $this->compressionRatio,
75+
'no_speech_prob' => $this->noSpeechProb,
76+
'transient' => $this->transient,
77+
];
78+
}
79+
}

tests/Fixtures/Audio.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,79 @@ function audioTranscriptionSrt(): string
7373
SRT;
7474
}
7575

76+
/**
77+
* @return array<string, mixed>
78+
*/
79+
function audioTranslationVerboseJson(): array
80+
{
81+
return [
82+
'task' => 'translate',
83+
'language' => 'english',
84+
'duration' => 2.95,
85+
'segments' => [
86+
[
87+
'id' => 0,
88+
'seek' => 0,
89+
'start' => 0.0,
90+
'end' => 4.0,
91+
'text' => ' Hello, how are you?',
92+
'tokens' => [
93+
50364,
94+
2425,
95+
11,
96+
577,
97+
366,
98+
291,
99+
30,
100+
50564,
101+
],
102+
'temperature' => 0.0,
103+
'avg_logprob' => -0.45045216878255206,
104+
'compression_ratio' => 0.7037037037037037,
105+
'no_speech_prob' => 0.1076972484588623,
106+
'transient' => false,
107+
],
108+
],
109+
'text' => 'Hello, how are you?',
110+
];
111+
}
112+
113+
/**
114+
* @return array<string, string>
115+
*/
116+
function audioTranslationJson(): array
117+
{
118+
return [
119+
'text' => 'Hello, how are you?',
120+
];
121+
}
122+
123+
function audioTranslationText(): string
124+
{
125+
return 'Hello, how are you?';
126+
}
127+
128+
function audioTranslationVtt(): string
129+
{
130+
return <<<'VTT'
131+
WEBVTT
132+
133+
00:00:00.000 --> 00:00:04.000
134+
Hello, how are you?
135+
136+
VTT;
137+
}
138+
139+
function audioTranslationSrt(): string
140+
{
141+
return <<<'SRT'
142+
1
143+
00:00:00,000 --> 00:00:04,000
144+
Hello, how are you?
145+
146+
SRT;
147+
}
148+
76149
/**
77150
* @return resource
78151
*/

tests/Resources/Audio.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
use OpenAI\Responses\Audio\TranscriptionResponse;
44
use OpenAI\Responses\Audio\TranscriptionResponseSegment;
5+
use OpenAI\Responses\Audio\TranslationResponse;
6+
use OpenAI\Responses\Audio\TranslationResponseSegment;
57

68
test('transcribe to text', function () {
79
$client = mockClient('POST', 'audio/transcriptions', [
@@ -86,3 +88,87 @@
8688
->noSpeechProb->toBe(0.1076972484588623)
8789
->transient->toBeFalse();
8890
});
91+
92+
test('translate to text', function () {
93+
$client = mockClient('POST', 'audio/translations', [
94+
'file' => audioFileResource(),
95+
'model' => 'whisper-1',
96+
'response_format' => 'text',
97+
], audioTranslationText());
98+
99+
$result = $client->audio()->translate([
100+
'file' => audioFileResource(),
101+
'model' => 'whisper-1',
102+
'response_format' => 'text',
103+
]);
104+
105+
expect($result)
106+
->toBeInstanceOf(TranslationResponse::class)
107+
->task->toBeNull()
108+
->language->toBeNull()
109+
->duration->toBeNull()
110+
->segments->toBeEmpty()
111+
->text->toBe('Hello, how are you?');
112+
});
113+
114+
test('translate to json', function () {
115+
$client = mockClient('POST', 'audio/translations', [
116+
'file' => audioFileResource(),
117+
'model' => 'whisper-1',
118+
'response_format' => 'json',
119+
], audioTranslationJson());
120+
121+
$result = $client->audio()->translate([
122+
'file' => audioFileResource(),
123+
'model' => 'whisper-1',
124+
'response_format' => 'json',
125+
]);
126+
127+
expect($result)
128+
->toBeInstanceOf(TranslationResponse::class)
129+
->task->toBeNull()
130+
->language->toBeNull()
131+
->duration->toBeNull()
132+
->segments->toBeEmpty()
133+
->text->toBe('Hello, how are you?');
134+
});
135+
136+
test('translate to verbose json', function () {
137+
$client = mockClient('POST', 'audio/translations', [
138+
'file' => audioFileResource(),
139+
'model' => 'whisper-1',
140+
'response_format' => 'verbose_json',
141+
], audioTranslationVerboseJson());
142+
143+
$result = $client->audio()->translate([
144+
'file' => audioFileResource(),
145+
'model' => 'whisper-1',
146+
'response_format' => 'verbose_json',
147+
]);
148+
149+
expect($result)
150+
->toBeInstanceOf(TranslationResponse::class)
151+
->task->toBe('translate')
152+
->language->toBe('english')
153+
->duration->toBe(2.95)
154+
->segments->toBeArray()
155+
->segments->toHaveCount(1)
156+
->segments->each->toBeInstanceOf(TranslationResponseSegment::class)
157+
->text->toBe('Hello, how are you?');
158+
159+
expect($result->segments[0])
160+
->toBeInstanceOf(TranslationResponseSegment::class)
161+
->id->toBe(0)
162+
->seek->toBe(0)
163+
->start->toBe(0.0)
164+
->end->toBe(4.0)
165+
->text->toBe(' Hello, how are you?')
166+
->tokens->toBeArray()
167+
->tokens->toHaveCount(8)
168+
->tokens->toBe([50364, 2425, 11, 577, 366, 291, 30, 50564])
169+
->temperature->toBe(0.0)
170+
->avgLogprob->toBe(-0.45045216878255206)
171+
->compressionRatio->toBe(0.7037037037037037)
172+
->noSpeechProb->toBe(0.1076972484588623)
173+
->transient->toBeFalse();
174+
});

0 commit comments

Comments
 (0)