Skip to content

Commit 6b69847

Browse files
committed
Merge branch 'pr/696’ into main-pull-requests
llms: Add support for using the whisper model to transcribe audio Squashed commit of the following: Author: Alexandre E Souza <alexandre@dev2learn.com> Date: Fri Mar 29 23:27:33 2024 -0300 chore: remove language chore: move whiper to documentloaders feat: update comments feat:add link for audio formats llms: Wisper model
1 parent b9b6b35 commit 6b69847

4 files changed

Lines changed: 276 additions & 1 deletion

File tree

documentloaders/sample.mp3

137 KB
Binary file not shown.

documentloaders/whisper.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package documentloaders
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"mime/multipart"
10+
"net/http"
11+
"net/url"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
"time"
16+
17+
"github.com/tmc/langchaingo/schema"
18+
"github.com/tmc/langchaingo/textsplitter"
19+
)
20+
21+
// WhisperOpenAILoader is a struct for loading and transcribing audio files using Whisper OpenAI model.
22+
type WhisperOpenAILoader struct {
23+
model string // the model to use for transcription
24+
audioFilePath string // path to the audio file
25+
language string // language of the audio
26+
temperature float64 // transcription temperature
27+
token string // authentication token for OpenAI API
28+
}
29+
30+
// Ensure WhisperOpenAILoader implements the Loader interface.
31+
var _ Loader = &WhisperOpenAILoader{}
32+
33+
// TranscribeAudioResponse represents the JSON response from the transcription API.
34+
type TranscribeAudioResponse struct {
35+
Text string `json:"text"`
36+
}
37+
38+
// WhisperOpenAIOption defines a function type for configuring WhisperOpenAILoader.
39+
type WhisperOpenAIOption func(loader *WhisperOpenAILoader)
40+
41+
// NewWhisperOpenAI creates a new WhisperOpenAILoader with given API key and options.
42+
func NewWhisperOpenAI(apiKey string, opts ...WhisperOpenAIOption) *WhisperOpenAILoader {
43+
loader := &WhisperOpenAILoader{
44+
model: "whisper-1",
45+
temperature: 0.7,
46+
language: "en",
47+
token: apiKey,
48+
}
49+
// Apply options to configure the loader.
50+
for _, opt := range opts {
51+
opt(loader)
52+
}
53+
54+
return loader
55+
}
56+
57+
// WithModel sets the model for the WhisperOpenAILoader.
58+
func WithModel(model string) WhisperOpenAIOption {
59+
return func(w *WhisperOpenAILoader) {
60+
w.model = model
61+
}
62+
}
63+
64+
// WithAudioPath sets the audio file path for the WhisperOpenAILoader.
65+
func WithAudioPath(path string) WhisperOpenAIOption {
66+
return func(w *WhisperOpenAILoader) {
67+
w.audioFilePath = path
68+
}
69+
}
70+
71+
// WithLanguage allows setting a custom language.
72+
// doc for language: https://platform.openai.com/docs/guides/speech-to-text/supported-languages
73+
func WithLanguage(language string) WhisperOpenAIOption {
74+
return func(opts *WhisperOpenAILoader) {
75+
opts.language = language
76+
}
77+
}
78+
79+
// WithTemperature sets the transcription temperature for the WhisperOpenAILoader.
80+
func WithTemperature(temperature float64) WhisperOpenAIOption {
81+
return func(w *WhisperOpenAILoader) {
82+
w.temperature = temperature
83+
}
84+
}
85+
86+
func (c *WhisperOpenAILoader) Load(ctx context.Context) ([]schema.Document, error) {
87+
if strings.Contains(c.audioFilePath, "http") {
88+
audioFilePath, err := downloadFileFromURL(c.audioFilePath)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
c.audioFilePath = audioFilePath
94+
}
95+
96+
transcribe, err := c.transcribe(ctx, c.audioFilePath)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
// create a virtual file
102+
tmpOutputFile, err := os.CreateTemp("", "*.txt")
103+
if err != nil {
104+
return nil, fmt.Errorf("erro ao criar arquivo temporário de texto: %w", err)
105+
}
106+
107+
defer os.Remove(tmpOutputFile.Name())
108+
109+
// Write in virtual file
110+
if _, err := tmpOutputFile.Write(transcribe); err != nil {
111+
return nil, fmt.Errorf("erro ao escrever no arquivo temporário de texto: %w", err)
112+
}
113+
114+
// read file
115+
file, err := os.Open(tmpOutputFile.Name())
116+
if err != nil {
117+
return nil, fmt.Errorf("erro ao ler o arquivo de texto gerado: %w", err)
118+
}
119+
txtLoader := NewText(file)
120+
121+
return txtLoader.Load(ctx)
122+
}
123+
124+
func (c *WhisperOpenAILoader) LoadAndSplit(ctx context.Context, splitter textsplitter.TextSplitter) ([]schema.Document, error) {
125+
docs, err := c.Load(ctx)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
return textsplitter.SplitDocuments(splitter, docs)
131+
}
132+
133+
// downloadFileFromURL downloads a file from the provided URL and saves it to a temporary file.
134+
// It returns the path to the temporary file and any error encountered.
135+
//
136+
// nolint
137+
func downloadFileFromURL(fileURL string) (string, error) {
138+
parsedURL, err := url.Parse(fileURL)
139+
if err != nil {
140+
return "", fmt.Errorf("failed to parse URL: %w", err)
141+
}
142+
143+
// Additional schema verification can be performed here if necessary
144+
145+
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
146+
return "", fmt.Errorf("URL scheme must be HTTP or HTTPS")
147+
}
148+
149+
// Configuring an http.Client with timeout
150+
netClient := &http.Client{
151+
Timeout: time.Second * 10, // Set the timeout as needed
152+
}
153+
154+
resp, err := netClient.Get(fileURL)
155+
if err != nil {
156+
return "", err
157+
}
158+
defer resp.Body.Close()
159+
160+
// Rest of the code for file manipulation...
161+
tmpFile, err := os.CreateTemp("", "downloaded_file_*") // Adjust the default according to the file type
162+
if err != nil {
163+
return "", err
164+
}
165+
defer tmpFile.Close()
166+
167+
_, err = io.Copy(tmpFile, resp.Body)
168+
if err != nil {
169+
return "", err
170+
}
171+
172+
return tmpFile.Name(), nil
173+
}
174+
175+
// transcribe performs the audio file transcription using the Whisper OpenAI model.
176+
func (c *WhisperOpenAILoader) transcribe(ctx context.Context, audioFilePath string) ([]byte, error) {
177+
payload := &bytes.Buffer{}
178+
writer := multipart.NewWriter(payload)
179+
file, err := os.Open(audioFilePath)
180+
if err != nil {
181+
return nil, err
182+
}
183+
defer file.Close()
184+
185+
// Create a form file part in the multipart writer.
186+
part1, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
187+
if err != nil {
188+
return nil, err
189+
}
190+
if _, err = io.Copy(part1, file); err != nil {
191+
return nil, err
192+
}
193+
194+
// Add other fields to the multipart form.
195+
_ = writer.WriteField("model", c.model)
196+
_ = writer.WriteField("response_format", "json")
197+
_ = writer.WriteField("temperature", fmt.Sprintf("%f", c.temperature))
198+
_ = writer.WriteField("language", c.language)
199+
if err = writer.Close(); err != nil {
200+
return nil, err
201+
}
202+
203+
client := &http.Client{}
204+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.openai.com/v1/audio/transcriptions", payload)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
// Set request headers.
210+
req.Header.Set("Authorization", "Bearer "+c.token)
211+
req.Header.Set("Content-Type", writer.FormDataContentType()) // Correctly set the Content-Type for multipart form data.
212+
213+
res, err := client.Do(req)
214+
if err != nil {
215+
return nil, err
216+
}
217+
defer res.Body.Close()
218+
219+
body, err := io.ReadAll(res.Body)
220+
if err != nil {
221+
return nil, err
222+
}
223+
var transcriptionResponse TranscribeAudioResponse
224+
if err = json.Unmarshal(body, &transcriptionResponse); err != nil {
225+
return nil, err
226+
}
227+
return []byte(transcriptionResponse.Text), nil
228+
}

documentloaders/whisper_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package documentloaders
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestTranscription(t *testing.T) {
13+
t.Parallel()
14+
if openaiKey := os.Getenv("OPENAI_API_KEY"); openaiKey == "" {
15+
t.Skip("OPENAI_API_KEY not set")
16+
}
17+
t.Run("Test with local file", func(t *testing.T) {
18+
t.Parallel()
19+
audioFilePath := "./sample.mp3"
20+
_, err := os.Stat(audioFilePath)
21+
require.NoError(t, err)
22+
opts := []WhisperOpenAIOption{
23+
WithAudioPath(audioFilePath),
24+
}
25+
whisper := NewWhisperOpenAI(os.Getenv("OPENAI_API_KEY"), opts...)
26+
27+
rsp, err := whisper.Load(context.Background())
28+
require.NoError(t, err)
29+
30+
assert.NotEmpty(t, rsp)
31+
})
32+
33+
t.Run("Test from url", func(t *testing.T) {
34+
t.Parallel()
35+
audioURL := "https://github.com/AssemblyAI-Examples/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3"
36+
37+
opts := []WhisperOpenAIOption{
38+
WithAudioPath(audioURL),
39+
}
40+
whisper := NewWhisperOpenAI(os.Getenv("OPENAI_API_KEY"), opts...)
41+
42+
rsp, err := whisper.Load(context.Background())
43+
require.NoError(t, err)
44+
45+
assert.NotEmpty(t, rsp)
46+
})
47+
}

llms/openai/multicontent_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"github.com/tmc/langchaingo/llms"
1414
)
1515

16-
func newTestClient(t *testing.T, opts ...Option) llms.Model {
16+
func newTestClient(t *testing.T, opts ...Option) *LLM {
1717
t.Helper()
1818

1919
httprr.SkipIfNoCredentialsAndRecordingMissing(t, "OPENAI_API_KEY")

0 commit comments

Comments
 (0)