From ecdb3a6d054b408a3a0b9b2b55aef4ac21791bfd Mon Sep 17 00:00:00 2001 From: Richard Park <51494936+richardpark-msft@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:35:11 -0800 Subject: [PATCH] [azopenai] Fixing issue where you can't use whisper with m4a files. (#22210) Fixing issue where you can't use whisper with m4a files. * It's one of the formats that doesn't seem to be recognized without an explicit file extension, which you can pass via Filename * My tests were too heavily dependent on implementation details of the models. Changing this out to check that things are working correctly without checking the exact contents of the response. * Also, rerecorded tests since we're doing multiple audio tests as well. Fixes #22195 --- sdk/ai/azopenai/CHANGELOG.md | 3 + sdk/ai/azopenai/assets.json | 2 +- sdk/ai/azopenai/client_audio_test.go | 331 ++++++++++-------- sdk/ai/azopenai/client_completions_test.go | 40 +-- sdk/ai/azopenai/client_shared_test.go | 4 +- sdk/ai/azopenai/custom_client_audio.go | 17 +- sdk/ai/azopenai/custom_client_test.go | 3 +- ...diofiles_myVoiceIsMyPassportVerifyMe01.m4a | Bin 0 -> 30246 bytes 8 files changed, 227 insertions(+), 173 deletions(-) create mode 100644 sdk/ai/azopenai/testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a diff --git a/sdk/ai/azopenai/CHANGELOG.md b/sdk/ai/azopenai/CHANGELOG.md index 8cba1e8685ed..17324cfcb95d 100644 --- a/sdk/ai/azopenai/CHANGELOG.md +++ b/sdk/ai/azopenai/CHANGELOG.md @@ -8,6 +8,9 @@ ### Bugs Fixed +- `AudioTranscriptionOptions.Filename` and `AudioTranslationOptions.Filename` fields are now properly propagated, allowing + for disambiguating the format of an audio file when OpenAI can't detect it. (PR#22210) + ### Other Changes ## 0.4.0 (2023-12-11) diff --git a/sdk/ai/azopenai/assets.json b/sdk/ai/azopenai/assets.json index 0326a3ded597..7cbfc913d854 100644 --- a/sdk/ai/azopenai/assets.json +++ b/sdk/ai/azopenai/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "go", "TagPrefix": "go/ai/azopenai", - "Tag": "go/ai/azopenai_b42da78821" + "Tag": "go/ai/azopenai_85a01b7ac6" } diff --git a/sdk/ai/azopenai/client_audio_test.go b/sdk/ai/azopenai/client_audio_test.go index f49a9e4065ad..fb6d3a2e1dd6 100644 --- a/sdk/ai/azopenai/client_audio_test.go +++ b/sdk/ai/azopenai/client_audio_test.go @@ -8,7 +8,9 @@ package azopenai_test import ( "context" + "fmt" "os" + "path/filepath" "testing" "github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai" @@ -24,20 +26,27 @@ func TestClient_GetAudioTranscription_AzureOpenAI(t *testing.T) { func TestClient_GetAudioTranscription_OpenAI(t *testing.T) { client := newOpenAIClientForTest(t) - mp3Bytes, err := os.ReadFile(`testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`) - require.NoError(t, err) - - args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatVerboseJSON, openAI.Whisper.Model, mp3Bytes) - transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) + testFiles := []string{ + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a`, + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`, + } - require.NotEmpty(t, *transcriptResp.Text) - require.Greater(t, *transcriptResp.Duration, float32(0.0)) - require.NotEmpty(t, *transcriptResp.Language) - require.NotEmpty(t, transcriptResp.Segments) - require.NotEmpty(t, transcriptResp.Segments[0]) - require.NotEmpty(t, transcriptResp.Task) + for _, audioFile := range testFiles { + t.Run(fmt.Sprintf("verbose (%s)", filepath.Ext(audioFile)), func(t *testing.T) { + args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatVerboseJSON, openAI.Whisper.Model, audioFile) + + transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + require.Greater(t, *transcriptResp.Duration, float32(0.0)) + require.NotEmpty(t, *transcriptResp.Language) + require.NotEmpty(t, transcriptResp.Segments) + require.NotEmpty(t, transcriptResp.Segments[0]) + require.NotEmpty(t, transcriptResp.Task) + }) + } } func TestClient_GetAudioTranslation_AzureOpenAI(t *testing.T) { @@ -48,154 +57,192 @@ func TestClient_GetAudioTranslation_AzureOpenAI(t *testing.T) { func TestClient_GetAudioTranslation_OpenAI(t *testing.T) { client := newOpenAIClientForTest(t) - mp3Bytes, err := os.ReadFile(`testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`) - require.NoError(t, err) - - args := newTranslationOptions(azopenai.AudioTranslationFormatVerboseJSON, openAI.Whisper.Model, mp3Bytes) - transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) + testFiles := []string{ + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a`, + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`, + } - require.NotEmpty(t, *transcriptResp.Text) - require.Greater(t, *transcriptResp.Duration, float32(0.0)) - require.NotEmpty(t, *transcriptResp.Language) - require.NotEmpty(t, transcriptResp.Segments) - require.NotEmpty(t, transcriptResp.Segments[0]) - require.NotEmpty(t, transcriptResp.Task) + for _, audioFile := range testFiles { + t.Run(fmt.Sprintf("verbose (%s)", filepath.Ext(audioFile)), func(t *testing.T) { + args := newTranslationOptions(azopenai.AudioTranslationFormatVerboseJSON, openAI.Whisper.Model, audioFile) + transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + require.Greater(t, *transcriptResp.Duration, float32(0.0)) + require.NotEmpty(t, *transcriptResp.Language) + require.NotEmpty(t, transcriptResp.Segments) + require.NotEmpty(t, transcriptResp.Segments[0]) + require.NotEmpty(t, transcriptResp.Task) + }) + } } func runTranscriptionTests(t *testing.T, client *azopenai.Client, model string) { - mp3Bytes, err := os.ReadFile(`testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`) - require.NoError(t, err) - - t.Run(string(azopenai.AudioTranscriptionFormatText), func(t *testing.T) { - args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatText, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatSrt), func(t *testing.T) { - args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatSrt, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatVtt), func(t *testing.T) { - args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatVtt, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatVerboseJSON), func(t *testing.T) { - args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatVerboseJSON, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - require.Greater(t, *transcriptResp.Duration, float32(0.0)) - require.NotEmpty(t, *transcriptResp.Language) - require.NotEmpty(t, transcriptResp.Segments) - require.NotEmpty(t, transcriptResp.Segments[0]) - require.NotEmpty(t, transcriptResp.Task) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatJSON), func(t *testing.T) { - args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatJSON, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) - }) + testFiles := []string{ + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a`, + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`, + } + + for _, audioFile := range testFiles { + ext := filepath.Ext(audioFile) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatText, ext), func(t *testing.T) { + args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatText, model, audioFile) + transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatSrt, ext), func(t *testing.T) { + args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatSrt, model, audioFile) + transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatVtt, ext), func(t *testing.T) { + args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatVtt, model, audioFile) + transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatVerboseJSON, ext), func(t *testing.T) { + args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatVerboseJSON, model, audioFile) + transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + require.Greater(t, *transcriptResp.Duration, float32(0.0)) + require.NotEmpty(t, *transcriptResp.Language) + require.NotEmpty(t, transcriptResp.Segments) + require.NotEmpty(t, transcriptResp.Segments[0]) + require.NotEmpty(t, transcriptResp.Task) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatJSON, ext), func(t *testing.T) { + args := newTranscriptionOptions(azopenai.AudioTranscriptionFormatJSON, model, audioFile) + transcriptResp, err := client.GetAudioTranscription(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranscription(t, transcriptResp.AudioTranscription) + }) + } } func runTranslationTests(t *testing.T, client *azopenai.Client, model string) { - mp3Bytes, err := os.ReadFile(`testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`) - require.NoError(t, err) - - t.Run(string(azopenai.AudioTranscriptionFormatText), func(t *testing.T) { - args := newTranslationOptions(azopenai.AudioTranslationFormatText, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatSrt), func(t *testing.T) { - args := newTranslationOptions(azopenai.AudioTranslationFormatSrt, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatVtt), func(t *testing.T) { - args := newTranslationOptions(azopenai.AudioTranslationFormatVtt, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatVerboseJSON), func(t *testing.T) { - args := newTranslationOptions(azopenai.AudioTranslationFormatVerboseJSON, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - require.Greater(t, *transcriptResp.Duration, float32(0.0)) - require.NotEmpty(t, *transcriptResp.Language) - require.NotEmpty(t, transcriptResp.Segments) - require.NotEmpty(t, transcriptResp.Segments[0]) - require.NotEmpty(t, transcriptResp.Task) - }) - - t.Run(string(azopenai.AudioTranscriptionFormatJSON), func(t *testing.T) { - args := newTranslationOptions(azopenai.AudioTranslationFormatJSON, model, mp3Bytes) - transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) - require.NoError(t, err) - require.NotEmpty(t, transcriptResp) - - require.NotEmpty(t, *transcriptResp.Text) - requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) - }) + testFiles := []string{ + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a`, + `testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.mp3`, + } + + for _, audioFile := range testFiles { + ext := filepath.Ext(audioFile) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatText, ext), func(t *testing.T) { + args := newTranslationOptions(azopenai.AudioTranslationFormatText, model, audioFile) + transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatSrt, ext), func(t *testing.T) { + args := newTranslationOptions(azopenai.AudioTranslationFormatSrt, model, audioFile) + transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatVtt, ext), func(t *testing.T) { + args := newTranslationOptions(azopenai.AudioTranslationFormatVtt, model, audioFile) + transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatVerboseJSON, ext), func(t *testing.T) { + args := newTranslationOptions(azopenai.AudioTranslationFormatVerboseJSON, model, audioFile) + transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + require.Greater(t, *transcriptResp.Duration, float32(0.0)) + require.NotEmpty(t, *transcriptResp.Language) + require.NotEmpty(t, transcriptResp.Segments) + require.NotEmpty(t, transcriptResp.Segments[0]) + require.NotEmpty(t, transcriptResp.Task) + }) + + t.Run(fmt.Sprintf("%s (%s)", azopenai.AudioTranscriptionFormatJSON, ext), func(t *testing.T) { + args := newTranslationOptions(azopenai.AudioTranslationFormatJSON, model, audioFile) + transcriptResp, err := client.GetAudioTranslation(context.Background(), args, nil) + require.NoError(t, err) + require.NotEmpty(t, transcriptResp) + + require.NotEmpty(t, *transcriptResp.Text) + requireEmptyAudioTranslation(t, transcriptResp.AudioTranslation) + }) + } } -func newTranscriptionOptions(format azopenai.AudioTranscriptionFormat, model string, mp3Bytes []byte) azopenai.AudioTranscriptionOptions { +func newTranscriptionOptions(format azopenai.AudioTranscriptionFormat, model string, path string) azopenai.AudioTranscriptionOptions { + audioBytes, err := os.ReadFile(path) + + if err != nil { + panic(err) + } + return azopenai.AudioTranscriptionOptions{ DeploymentName: to.Ptr(model), - File: mp3Bytes, + File: audioBytes, + Filename: &path, ResponseFormat: &format, Language: to.Ptr("en"), Temperature: to.Ptr[float32](0.0), } } -func newTranslationOptions(format azopenai.AudioTranslationFormat, model string, mp3Bytes []byte) azopenai.AudioTranslationOptions { +func newTranslationOptions(format azopenai.AudioTranslationFormat, model string, path string) azopenai.AudioTranslationOptions { + audioBytes, err := os.ReadFile(path) + + if err != nil { + panic(err) + } + + var filename *string + + if filepath.Ext(path) != ".mp3" { + filename = &path + } + return azopenai.AudioTranslationOptions{ DeploymentName: to.Ptr(model), - File: mp3Bytes, + File: audioBytes, + Filename: filename, ResponseFormat: &format, Temperature: to.Ptr[float32](0.0), } diff --git a/sdk/ai/azopenai/client_completions_test.go b/sdk/ai/azopenai/client_completions_test.go index 13242d371f2c..96a408b28425 100644 --- a/sdk/ai/azopenai/client_completions_test.go +++ b/sdk/ai/azopenai/client_completions_test.go @@ -45,33 +45,23 @@ func testGetCompletions(t *testing.T, client *azopenai.Client, isAzure bool) { skipNowIfThrottled(t, err) require.NoError(t, err) - want := azopenai.GetCompletionsResponse{ - Completions: azopenai.Completions{ - Choices: []azopenai.Choice{ - { - Text: to.Ptr("\n\nAzure OpenAI is a platform from Microsoft that provides access to OpenAI's artificial intelligence (AI) technologies. It enables developers to build, train, and deploy AI models in the cloud. Azure OpenAI provides access to OpenAI's powerful AI technologies, such as GPT-3, which can be used to create natural language processing (NLP) applications, computer vision models, and reinforcement learning models."), - Index: to.Ptr(int32(0)), - FinishReason: to.Ptr(azopenai.CompletionsFinishReason("stop")), - LogProbs: nil, - }, - }, - Usage: &azopenai.CompletionsUsage{ - CompletionTokens: to.Ptr(int32(85)), - PromptTokens: to.Ptr(int32(6)), - TotalTokens: to.Ptr(int32(91)), - }, - }, - } + // we'll do a general check here - as models change the answers can also change, token usages are different, + // etc... So we'll just make sure data is coming back and is reasonable. + require.NotZero(t, *resp.Completions.Usage.PromptTokens) + require.NotZero(t, *resp.Completions.Usage.CompletionTokens) + require.NotZero(t, *resp.Completions.Usage.TotalTokens) + require.Equal(t, int32(0), *resp.Completions.Choices[0].Index) + require.Equal(t, azopenai.CompletionsFinishReasonStopped, *resp.Completions.Choices[0].FinishReason) + + require.NotEmpty(t, *resp.Completions.Choices[0].Text) if isAzure { - want.Choices[0].ContentFilterResults = (*azopenai.ContentFilterResultsForChoice)(safeContentFilter) - want.PromptFilterResults = []azopenai.ContentFilterResultsForPrompt{ - {PromptIndex: to.Ptr[int32](0), ContentFilterResults: safeContentFilterResultDetailsForPrompt}, - } + require.Equal(t, safeContentFilter, resp.Completions.Choices[0].ContentFilterResults) + require.Equal(t, []azopenai.ContentFilterResultsForPrompt{ + { + PromptIndex: to.Ptr[int32](0), + ContentFilterResults: safeContentFilterResultDetailsForPrompt, + }}, resp.PromptFilterResults) } - want.ID = resp.Completions.ID - want.Created = resp.Completions.Created - - require.Equal(t, want, resp) } diff --git a/sdk/ai/azopenai/client_shared_test.go b/sdk/ai/azopenai/client_shared_test.go index 5651136e5792..400ddaa01f57 100644 --- a/sdk/ai/azopenai/client_shared_test.go +++ b/sdk/ai/azopenai/client_shared_test.go @@ -246,8 +246,8 @@ func initEnvVars() { openAI.Vision = azureOpenAI.Vision - azureOpenAI.Completions = "text-davinci-003" - openAI.Completions = "text-davinci-003" + azureOpenAI.Completions = "gpt-35-turbo-instruct" + openAI.Completions = "gpt-3.5-turbo-instruct" azureOpenAI.ChatCompletions = "gpt-35-turbo-0613" azureOpenAI.ChatCompletionsLegacyFunctions = "gpt-4-0613" diff --git a/sdk/ai/azopenai/custom_client_audio.go b/sdk/ai/azopenai/custom_client_audio.go index caf80ef65768..d9ce56f5177d 100644 --- a/sdk/ai/azopenai/custom_client_audio.go +++ b/sdk/ai/azopenai/custom_client_audio.go @@ -44,6 +44,7 @@ func (client *Client) GetAudioTranscription(ctx context.Context, body AudioTrans audioStream := streaming.NopCloser(bytes.NewReader(body.File)) resp, err := client.getAudioTranscriptionInternal(ctx, getDeployment(body), audioStream, &getAudioTranscriptionInternalOptions{ + Filename: body.Filename, Language: body.Language, Model: body.DeploymentName, Prompt: body.Prompt, @@ -79,6 +80,7 @@ func (client *Client) GetAudioTranslation(ctx context.Context, body AudioTransla audioStream := streaming.NopCloser(bytes.NewReader(body.File)) resp, err := client.getAudioTranslationInternal(ctx, getDeployment(body), audioStream, &getAudioTranslationInternalOptions{ + Filename: body.Filename, Model: body.DeploymentName, Prompt: body.Prompt, ResponseFormat: body.ResponseFormat, @@ -106,7 +108,20 @@ func setMultipartFormData[T getAudioTranscriptionInternalOptions | getAudioTrans return err } - if err := writeContent("file", "audio.mp3", file); err != nil { + var filename = "audio.mp3" + + switch opt := any(options).(type) { + case getAudioTranscriptionInternalOptions: + if opt.Filename != nil { + filename = *opt.Filename + } + case getAudioTranslationInternalOptions: + if opt.Filename != nil { + filename = *opt.Filename + } + } + + if err := writeContent("file", filename, file); err != nil { return err } diff --git a/sdk/ai/azopenai/custom_client_test.go b/sdk/ai/azopenai/custom_client_test.go index 9d851d213171..7b024096c41c 100644 --- a/sdk/ai/azopenai/custom_client_test.go +++ b/sdk/ai/azopenai/custom_client_test.go @@ -137,9 +137,8 @@ func testGetCompletionsStream(t *testing.T, client *azopenai.Client, tv testVars } } got := sb.String() - const want = "\n\nAzure OpenAI is a platform from Microsoft that provides access to OpenAI's artificial intelligence (AI) technologies. It enables developers to build, train, and deploy AI models in the cloud. Azure OpenAI provides access to OpenAI's powerful AI technologies, such as GPT-3, which can be used to create natural language processing (NLP) applications, computer vision models, and reinforcement learning models." - require.Equal(t, want, got) + require.NotEmpty(t, got) // there's no strict requirement of how the response is streamed so just // choosing something that's reasonable but will be lower than typical usage diff --git a/sdk/ai/azopenai/testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a b/sdk/ai/azopenai/testdata/sampledata_audiofiles_myVoiceIsMyPassportVerifyMe01.m4a new file mode 100644 index 0000000000000000000000000000000000000000..716d6d2067469949de16a3b32082b151674bb3a2 GIT binary patch literal 30246 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 literal 0 HcmV?d00001