Home → Blog → Speech to text in Go: working code on the standard library
Speech to text in Go: working code on the standard library
Go needs neither an SDK nor third-party packages to upload audio: a multipart request is twenty lines of standard library. Below is a complete function, sane error handling and a worker pool for chewing through an archive of recordings.
The upload function
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"time"
)
func transcribe(path, key string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
var body bytes.Buffer
w := multipart.NewWriter(&body)
part, _ := w.CreateFormFile("file", f.Name())
if _, err := io.Copy(part, f); err != nil {
return "", err
}
w.WriteField("model", "whisper-1")
w.Close()
req, _ := http.NewRequest("POST", "https://voicesscribe.com/v1/audio/transcriptions", &body)
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", w.FormDataContentType())
client := &http.Client{Timeout: 3 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var out struct {
Text string
Error struct{ Message string }
}
json.NewDecoder(resp.Body).Decode(&out)
if resp.StatusCode != 200 {
return "", fmt.Errorf("status %d: %s", resp.StatusCode, out.Error.Message)
}
return out.Text, nil
}
Note the client timeout: the default is none at all, so a request on a long recording can hang forever.
Handling errors
Error responses arrive in the familiar shape — an error object with a message field. Split failures into two groups: permanent ones (400, 401, 413) and temporary ones (429, 502, network drops). Repeating the first group is pointless; the second deserves a retry with a growing pause.
func withRetry(path, key string) (string, error) {
delay := 2 * time.Second
var last error
for i := 0; i < 4; i++ {
text, err := transcribe(path, key)
if err == nil {
return text, nil
}
last = err
if !temporary(err) {
break
}
time.Sleep(delay)
delay *= 2
}
return "", last
}A worker pool for archives
This is where Go shines: a bounded pool is a few lines and the bound protects you from rate-limit rejections.
files := make(chan string)
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for path := range files {
text, err := withRetry(path, key)
if err != nil {
log.Println(path, err)
continue
}
os.WriteFile(path+".txt", []byte(text), 0o644)
}
}()
}
Size the pool by the requests-per-second limit of your plan — launching a hundred goroutines only produces a hundred rejections.
Production notes
- Memory. The example buffers the whole body; for large files use io.Pipe and stream instead.
- Filenames. CreateFormFile needs a name with an extension or the format cannot be detected.
- Timeouts. Three minutes is a reasonable allowance for a file at the size limit.
- Secrets. Keep the key in the environment, never in source.
Try it on your own recordings. Sign-up takes a minute, and the free minutes are enough to judge the quality.
Get a free API keyFrequently asked questions
Is there an SDK for Go?
You do not need one. A multipart request on the standard library covers the whole API surface used here.
Why do long uploads get cut off?
http.Client has no default timeout, but proxies and load balancers do. Set your own timeout of a couple of minutes.
How do I pass the language or a prompt?
As extra form fields: language and prompt next to the model field.
How many goroutines should I run?
As many as your plan's rate limit allows. Exceeding it returns 429 and makes you redo the work.
Related reading
- Speech to text in Python: from one file to a working script — A working Python speech-to-text setup in ten minutes: install, the first request, response formats, error handling, and the mistakes that cost the most time.
- How to build a batch transcription pipeline for an audio archive — Transcribing thousands of recordings without losing any: a work queue, concurrency, retries, resuming after a crash and keeping the bill under control.
- Transcription API errors and what they actually mean — A walkthrough of transcription API status codes: why 401, 400, 413, 429 and 502 happen, how to fix each one and which failures are worth retrying.