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)
|
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.
|
// ImagesGenerations Creates an image given a prompt.
|
||||||
// https://platform.openai.com/docs/api-reference/images
|
// https://platform.openai.com/docs/api-reference/images
|
||||||
func (openai OpenAI) ImagesGenerations(prompt string, option map[string]interface{}) (interface{}, *exception.Exception) {
|
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 {
|
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)
|
res := req.Send("POST", option)
|
||||||
|
|
@ -461,7 +503,12 @@ func (openai OpenAI) postFileWithoutModel(path string, files map[string][]byte,
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, data := range files {
|
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)
|
res := req.Send("POST", option)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package openai
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -201,6 +202,47 @@ func TestAudioTranscriptions(t *testing.T) {
|
||||||
assert.Equal(t, "今晚打老虎", data.(map[string]interface{})["text"])
|
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) {
|
func TestImagesGenerations(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
@ -302,3 +344,10 @@ func audio(t *testing.T) string {
|
||||||
}
|
}
|
||||||
return base64.StdEncoding.EncodeToString(data)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ import (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
process.RegisterGroup("openai", map[string]process.Handler{
|
process.RegisterGroup("openai", map[string]process.Handler{
|
||||||
"tiktoken": ProcessTiktoken,
|
"tiktoken": ProcessTiktoken,
|
||||||
"embeddings": ProcessEmbeddings,
|
"embeddings": ProcessEmbeddings,
|
||||||
"chat.completions": ProcessChatCompletions,
|
"chat.completions": ProcessChatCompletions,
|
||||||
"audio.transcriptions": ProcessAudioTranscriptions,
|
"audio.transcriptions": ProcessAudioTranscriptions,
|
||||||
|
"audio.transcriptionsfile": ProcessAudioTranscriptionsFile,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,6 +78,45 @@ func ProcessAudioTranscriptions(process *process.Process) interface{} {
|
||||||
return res
|
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
|
// ProcessChatCompletions openai.chat.Completions
|
||||||
func ProcessChatCompletions(process *process.Process) interface{} {
|
func ProcessChatCompletions(process *process.Process) interface{} {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,45 @@ func TestProcessAudioTranscriptions(t *testing.T) {
|
||||||
assert.Equal(t, "今晚打老虎", data.(map[string]interface{})["text"])
|
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) {
|
func TestProcessChatCompletions(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue