Migrating from the OpenAI Whisper API to Speech Revolutions
OpenAI's transcription API is a single synchronous multipart POST to /v1/audio/transcriptions. It has a 25 MB file limit, and the current gpt-4o-transcribe model returns no word-level timestamps and no speaker diarization. Speech Revolutions returns the transcript, word timestamps, and speaker turns from a single call, for files up to 10 GB through the SDK.
On OpenAI, word timestamps require the older whisper-1 model (via response_format=verbose_json + timestamp_granularities), and speaker labels require a separate gpt-4o-transcribe-diarize model — gpt-4o-transcribe returns text only. Speech Revolutions returns result.text, result.words (with times), and result.utterances (speakers) in every response.
Authentication
OpenAI uses Authorization: Bearer <key>. Speech Revolutions uses X-API-Key. The SDK reads the key from the environment.
| OpenAI | Speech Revolutions |
|---|---|
Authorization: Bearer OPENAI_API_KEY | X-API-Key: SPEECHREVOLUTIONS_API_KEY |
OPENAI_API_KEY env var | SPEECHREVOLUTIONS_API_KEY |
Endpoint & method mapping
| OpenAI | Speech Revolutions REST | Speech Revolutions SDK |
|---|---|---|
POST /v1/audio/transcriptions (sync multipart) | POST /api/v1/transcribe (raw bytes in, transcript streamed back; up to 200 MB) | transcribe() (blocks until done) |
| — (response is inline) | GET /api/v1/jobs/{id} / /stream | get_transcript() / on_progress |
Upload differences and the 25 MB limit
OpenAI expects the audio as a multipart file field in the request body and rejects files over 25 MB, so you split or compress long recordings yourself. Speech Revolutions has no 25 MB limit: the SDKs upload files up to 10 GB to storage in parts. Pass a path, URL, or bytes.
If you split files to stay under 25 MB for OpenAI, remove that step. Send the whole recording to transcribe().
Response shape
With response_format=json, OpenAI's gpt-4o-transcribe returns only { text }, with no words, segments, or speakers. Speech Revolutions returns all of these:
| OpenAI field | Speech Revolutions |
|---|---|
text | result.text |
— (not returned by gpt-4o-transcribe) | result.words (word + start/end/speaker, seconds) |
| — (not returned) | result.utterances (speaker turns) |
language (with verbose_json) | result.languages |
Diarization
gpt-4o-transcribe does not diarize; OpenAI offers the separate gpt-4o-transcribe-diarize model (as of July 2026). Speech Revolutions diarizes in the same call: set speaker_labels (on by default) and read result.utterances. Zephyr ranks #1 on diarization error rate on every benchmark subset. See the benchmarks and the comparison table.
Timestamps
On OpenAI, word timestamps require whisper-1 with response_format=verbose_json and timestamp_granularities: ["word"]. On Speech Revolutions, word_timestamps is on by default and the times are returned on result.words in seconds, with no model change.
Language selection
OpenAI takes an ISO 639-1 language hint. Speech Revolutions auto-detects by default and returns the detected spans in result.languages. To skip detection, pass the same language parameter with an ISO 639-1 code. As with Whisper, a wrong code does not fail: the model translates into that language. See pinning the language.
Custom vocabulary
OpenAI biases transcription only through the free-text prompt (about 224 tokens maximum). Speech Revolutions takes a custom_vocabulary list of domain terms.
Side by side
From OpenAI — Python (openai_transcribe.py)
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY
with open("meeting.mp3", "rb") as f: # must be <= 25 MB
resp = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=f,
response_format="json",
)
print(resp.text)
# no word timestamps, no speakers from gpt-4o-transcribeTo Speech Revolutions — Python (stt_transcribe.py)
from speechrevolutions import SpeechRevolutions
client = SpeechRevolutions() # SPEECHREVOLUTIONS_API_KEY
result = client.transcribe("meeting.mp3", speaker_labels=True) # any size
print(result.text)
for w in result.words: # word timestamps, in seconds
print(w.text, w.start, w.end)
for u in result.utterances: # speaker turns
print(f"{u.speaker}: {u.text}")From OpenAI — JavaScript (openai-transcribe.ts)
import OpenAI from "openai";
import { createReadStream } from "node:fs";
const client = new OpenAI(); // OPENAI_API_KEY
const resp = await client.audio.transcriptions.create({
model: "gpt-4o-transcribe",
file: createReadStream("meeting.mp3"), // must be <= 25 MB
response_format: "json",
});
console.log(resp.text);
// no word timestamps, no speakers from gpt-4o-transcribeTo Speech Revolutions — JavaScript (stt-transcribe.ts)
import { SpeechRevolutions } from "speechrevolutions";
const client = new SpeechRevolutions(); // SPEECHREVOLUTIONS_API_KEY
const result = await client.transcribe("meeting.mp3", { speakerLabels: true }); // any size
console.log(result.text);
for (const w of result.words) console.log(w.text, w.start, w.end); // word timestamps, in seconds
for (const u of result.utterances) console.log(`${u.speaker}: ${u.text}`); // speaker turnsFrom OpenAI — Go (openai_transcribe.go)
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/openai/openai-go"
)
func main() {
ctx := context.Background()
client := openai.NewClient() // OPENAI_API_KEY
f, err := os.Open("meeting.mp3") // must be <= 25 MB
if err != nil {
log.Fatal(err)
}
defer f.Close()
resp, err := client.Audio.Transcriptions.New(ctx, openai.AudioTranscriptionNewParams{
Model: openai.AudioModelGPT4oTranscribe,
File: f,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text)
// no word timestamps, no speakers from gpt-4o-transcribe
}To Speech Revolutions — Go (stt_transcribe.go)
package main
import (
"context"
"fmt"
"log"
stt "github.com/speechrevolutions/speechrevolutions-go"
)
func main() {
ctx := context.Background()
client, err := stt.NewClient("") // SPEECHREVOLUTIONS_API_KEY
if err != nil {
log.Fatal(err)
}
result, err := client.Transcribe(ctx, "meeting.mp3", stt.TranscribeOptions{ // any size
SpeakerLabels: stt.Bool(true),
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text())
for _, w := range result.Words { // word timestamps, in seconds
if w.Start != nil && w.End != nil {
fmt.Println(w.Word, *w.Start, *w.End)
}
}
for _, u := range result.Utterances { // speaker turns
fmt.Printf("%s: %s\n", u.Speaker, u.Text)
}
}From OpenAI — C# (Program.cs)
using OpenAI.Audio;
var client = new AudioClient("gpt-4o-transcribe",
Environment.GetEnvironmentVariable("OPENAI_API_KEY")); // OPENAI_API_KEY
AudioTranscription resp = await client.TranscribeAudioAsync("meeting.mp3"); // must be <= 25 MB
Console.WriteLine(resp.Text);
// no word timestamps, no speakers from gpt-4o-transcribeTo Speech Revolutions — C# (Program.cs)
using SpeechRevolutions;
using var client = new SpeechRevolutionsClient(); // SPEECHREVOLUTIONS_API_KEY
var result = await client.TranscribeAsync(
"meeting.mp3", // any size
new TranscribeOptions { SpeakerLabels = true });
Console.WriteLine(result.Text);
foreach (var w in result.Words) // word timestamps, in seconds
Console.WriteLine($"{w.Text} {w.Start} {w.End}");
foreach (var u in result.Utterances) // speaker turns
Console.WriteLine($"{u.Speaker}: {u.Text}");Common pitfalls
- Auth prefix. Drop
Bearer; Speech Revolutions uses theX-API-Keyheader. - Expecting text only. Speech Revolutions returns
wordsandutterancesby default. You don't need a second model for timestamps or speakers. - Leftover 25 MB workarounds. Remove chunking or compression steps built for OpenAI.
- Prompt-based biasing. Replace the
promptterm list withcustom_vocabulary.
See the migration playbook for cutover, and the benchmarks for accuracy, diarization, and timestamp comparisons.