Add audio transcription file handling and related tests
- Introduce `AudioTranscriptionsFile` method in the OpenAI package to transcribe audio directly from a file path, improving memory efficiency. - Implement new test cases for `AudioTranscriptionsFile`, including scenarios for language options and error handling for non-existent files. - Update the process package to support the new transcription method, ensuring compatibility with existing functionality. - Enhance test coverage for audio transcription processes, validating expected outputs and error conditions.
This commit is contained in:
parent
bdcc3585e2
commit
ad311cad2a
4 changed files with 181 additions and 6 deletions
|
|
@ -253,6 +253,43 @@ func (openai OpenAI) AudioTranscriptions(dataBase64 string, option map[string]in
|
|||
return openai.postFile(openai.baseURL+"/audio/transcriptions", map[string][]byte{"file": data}, option)
|
||||
}
|
||||
|
||||
// AudioTranscriptionsFile Transcribes audio from an OS file path.
|
||||
// Unlike AudioTranscriptions which accepts base64-encoded data, this method
|
||||
// reads the file directly via streaming upload, avoiding 4× memory copies.
|
||||
// https://platform.openai.com/docs/api-reference/audio/create
|
||||
func (openai OpenAI) AudioTranscriptionsFile(filePath string, option map[string]interface{}) (interface{}, *exception.Exception) {
|
||||
|
||||
if option == nil {
|
||||
option = map[string]interface{}{}
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s%s", openai.host, openai.baseURL+"/audio/transcriptions")
|
||||
if _, ok := option["model"].(string); !ok {
|
||||
option["model"] = openai.model
|
||||
}
|
||||
|
||||
req := http.New(url)
|
||||
if openai.azure {
|
||||
req.WithHeader(map[string][]string{
|
||||
"Content-Type": {"multipart/form-data"},
|
||||
"api-key": {openai.key},
|
||||
})
|
||||
} else {
|
||||
req.WithHeader(map[string][]string{
|
||||
"Content-Type": {"multipart/form-data"},
|
||||
"Authorization": {fmt.Sprintf("Bearer %s", openai.key)},
|
||||
})
|
||||
}
|
||||
|
||||
req.AddFile("file", filePath)
|
||||
|
||||
res := req.Send("POST", option)
|
||||
if err := openai.isError(res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
// ImagesGenerations Creates an image given a prompt.
|
||||
// https://platform.openai.com/docs/api-reference/images
|
||||
func (openai OpenAI) ImagesGenerations(prompt string, option map[string]interface{}) (interface{}, *exception.Exception) {
|
||||
|
|
@ -437,7 +474,12 @@ func (openai OpenAI) postFile(path string, files map[string][]byte, option map[s
|
|||
}
|
||||
|
||||
for name, data := range files {
|
||||
req.AddFileBytes(name, fmt.Sprintf("%s.mp3", name), data)
|
||||
filename := fmt.Sprintf("%s.mp3", name)
|
||||
if fn, ok := option["filename"].(string); ok && fn != "" {
|
||||
filename = fn
|
||||
delete(option, "filename") // don't send as form field to API
|
||||
}
|
||||
req.AddFileBytes(name, filename, data)
|
||||
}
|
||||
|
||||
res := req.Send("POST", option)
|
||||
|
|
@ -461,7 +503,12 @@ func (openai OpenAI) postFileWithoutModel(path string, files map[string][]byte,
|
|||
}
|
||||
|
||||
for name, data := range files {
|
||||
req.AddFileBytes(name, fmt.Sprintf("%s.mp3", name), data)
|
||||
filename := fmt.Sprintf("%s.mp3", name)
|
||||
if fn, ok := option["filename"].(string); ok && fn != "" {
|
||||
filename = fn
|
||||
delete(option, "filename") // don't send as form field to API
|
||||
}
|
||||
req.AddFileBytes(name, filename, data)
|
||||
}
|
||||
|
||||
res := req.Send("POST", option)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package openai
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -201,6 +202,47 @@ func TestAudioTranscriptions(t *testing.T) {
|
|||
assert.Equal(t, "今晚打老虎", data.(map[string]interface{})["text"])
|
||||
}
|
||||
|
||||
func TestAudioTranscriptionsFile(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
openai := prepare(t, "whisper-1")
|
||||
filePath := audioFilePath(t)
|
||||
|
||||
data, err := openai.AudioTranscriptionsFile(filePath, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err.Message)
|
||||
}
|
||||
assert.Equal(t, "今晚打老虎", data.(map[string]interface{})["text"])
|
||||
}
|
||||
|
||||
func TestAudioTranscriptionsFile_WithLanguage(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
openai := prepare(t, "whisper-1")
|
||||
filePath := audioFilePath(t)
|
||||
|
||||
data, err := openai.AudioTranscriptionsFile(filePath, map[string]interface{}{"language": "zh"})
|
||||
if err != nil {
|
||||
t.Fatal(err.Message)
|
||||
}
|
||||
text, ok := data.(map[string]interface{})["text"].(string)
|
||||
assert.True(t, ok)
|
||||
assert.NotEmpty(t, text)
|
||||
t.Logf("Transcription with language=zh: %s", text)
|
||||
}
|
||||
|
||||
func TestAudioTranscriptionsFile_FileNotFound(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
openai := prepare(t, "whisper-1")
|
||||
_, err := openai.AudioTranscriptionsFile("/non/existent/audio.mp3", nil)
|
||||
assert.NotNil(t, err, "Expected error for non-existent file")
|
||||
t.Logf("Error for non-existent file: %s", err.Message)
|
||||
}
|
||||
|
||||
func TestImagesGenerations(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
|
@ -302,3 +344,10 @@ func audio(t *testing.T) string {
|
|||
}
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func audioFilePath(t *testing.T) string {
|
||||
stor := fs.MustGet("system")
|
||||
root := stor.Root()
|
||||
absPath := filepath.Join(root, "assets", "audio_transcriptions.mp3")
|
||||
return absPath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func init() {
|
|||
"embeddings": ProcessEmbeddings,
|
||||
"chat.completions": ProcessChatCompletions,
|
||||
"audio.transcriptions": ProcessAudioTranscriptions,
|
||||
"audio.transcriptionsfile": ProcessAudioTranscriptionsFile,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +78,45 @@ func ProcessAudioTranscriptions(process *process.Process) interface{} {
|
|||
return res
|
||||
}
|
||||
|
||||
// ProcessAudioTranscriptionsFile openai.audio.TranscriptionsFile
|
||||
// Transcribe audio from an OS file path (streaming upload, no base64 overhead).
|
||||
// This is the recommended way to call Whisper from TS scripts, consistent with
|
||||
// office.Parse / ffmpeg.* handler style.
|
||||
//
|
||||
// Args:
|
||||
// - connector string - AI connector name (e.g. "openai.whisper-1")
|
||||
// - filePath string - OS absolute path to the audio file
|
||||
// - options map - Optional: { language, model, ... }
|
||||
//
|
||||
// Returns: map[string]interface{} - Transcription result (e.g. {"text": "..."})
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// var result = Process("openai.audio.transcriptionsfile", "openai.whisper-1", "/abs/path/to/audio.mp3", {"language": "en"})
|
||||
func ProcessAudioTranscriptionsFile(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(2)
|
||||
connector := process.ArgsString(0)
|
||||
filePath := process.ArgsString(1)
|
||||
|
||||
options := map[string]interface{}{}
|
||||
if process.NumOfArgs() > 2 {
|
||||
if opts, ok := process.Args[2].(map[string]interface{}); ok {
|
||||
options = opts
|
||||
}
|
||||
}
|
||||
|
||||
ai, err := New(connector)
|
||||
if err != nil {
|
||||
exception.New("AudioTranscriptionsFile error: %s", 400, err).Throw()
|
||||
}
|
||||
|
||||
res, ex := ai.AudioTranscriptionsFile(filePath, options)
|
||||
if ex != nil {
|
||||
ex.Throw()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// ProcessChatCompletions openai.chat.Completions
|
||||
func ProcessChatCompletions(process *process.Process) interface{} {
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,45 @@ func TestProcessAudioTranscriptions(t *testing.T) {
|
|||
assert.Equal(t, "今晚打老虎", data.(map[string]interface{})["text"])
|
||||
}
|
||||
|
||||
func TestProcessAudioTranscriptionsFile(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
filePath := audioFilePath(t)
|
||||
args := []interface{}{"whisper-1", filePath}
|
||||
data := process.New("openai.audio.transcriptionsfile", args...).Run()
|
||||
assert.Equal(t, "今晚打老虎", data.(map[string]interface{})["text"])
|
||||
}
|
||||
|
||||
func TestProcessAudioTranscriptionsFile_WithOptions(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
filePath := audioFilePath(t)
|
||||
args := []interface{}{"whisper-1", filePath, map[string]interface{}{"language": "zh"}}
|
||||
data := process.New("openai.audio.transcriptionsfile", args...).Run()
|
||||
text, ok := data.(map[string]interface{})["text"].(string)
|
||||
assert.True(t, ok)
|
||||
assert.NotEmpty(t, text)
|
||||
t.Logf("ProcessAudioTranscriptionsFile with language=zh: %s", text)
|
||||
}
|
||||
|
||||
func TestProcessAudioTranscriptionsFile_InvalidConnector(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Error("Expected panic for invalid connector, but got none")
|
||||
}
|
||||
t.Logf("Correctly panicked with: %v", r)
|
||||
}()
|
||||
|
||||
args := []interface{}{"non-existent-connector", "/some/path.mp3"}
|
||||
process.New("openai.audio.transcriptionsfile", args...).Run()
|
||||
}
|
||||
|
||||
func TestProcessChatCompletions(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue