Migrating from ElevenLabs to Speech Revolutions
ElevenLabs' Scribe speech-to-text is a single synchronous multipart POST to /v1/speech-to-text. Word timestamps come back by default and diarize=true adds a speaker_id to each word. Speech Revolutions returns the same data (transcript, per-word times, and speakers), and the SDK does it in one transcribe() call. This guide maps authentication, upload, and the response shape, including the spacing tokens you no longer need to filter.
Your diarize flag works unchanged (Speech Revolutions accepts diarize as an alias for speaker_labels), and result.text maps directly. The main change is removing handling for ElevenLabs' spacing tokens: result.words contains only words.
Authentication
ElevenLabs authenticates with an xi-api-key header. Speech Revolutions uses X-API-Key. The SDK reads the key from the environment.
| ElevenLabs | Speech Revolutions |
|---|---|
xi-api-key: ELEVENLABS_API_KEY | X-API-Key: SPEECHREVOLUTIONS_API_KEY |
ELEVENLABS_API_KEY env var | SPEECHREVOLUTIONS_API_KEY |
Endpoint & method mapping
| ElevenLabs | Speech Revolutions REST | Speech Revolutions SDK |
|---|---|---|
POST /v1/speech-to-text (sync multipart, model_id) | 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
ElevenLabs takes the audio as a multipart file field with model_id in the form body. With Speech Revolutions, you pass a path, URL, or bytes to the SDK, which uploads the file to storage in parts. Speech Revolutions also reports live upload and transcribe progress (see live progress).
Response shape
ElevenLabs returns text plus a words[] array in which entries have a type of word or spacing; the spacing entries are not real words. Speakers appear as speaker_id strings (for example, speaker_0). Speech Revolutions' result.words contains only words, with string speaker labels.
| ElevenLabs field | Speech Revolutions |
|---|---|
text | result.text |
words[] where type == "word" (text, start, end, speaker_id) | result.words (text, start, end, speaker) — no spacing tokens |
words[] where type == "spacing" | — (not returned; no filtering needed) |
— (regroup by speaker_id yourself) | result.utterances (pre-grouped speaker turns) |
language_code | result.languages |
If your code skips word.type === "spacing" entries, delete that filter. result.words contains only words.
Diarization
ElevenLabs diarizes with diarize=true, adding a speaker_id per word. Speech Revolutions uses the same diarize flag (an alias for speaker_labels, on by default) and also groups words into result.utterances, so you don't need to rebuild turns from per-word IDs. Zephyr ranks #1 on diarization error rate (DER) on every benchmark subset. See the benchmarks and the comparison table.
Timestamps
Both return per-word start and end times in seconds by default. On the word-alignment benchmark, Zephyr's mean word-start error is 40 ms, against 58 ms for ElevenLabs.
Language selection
ElevenLabs takes language_code. Speech Revolutions auto-detects by default and detects language changes within a file, so a recording that switches languages mid-sentence is transcribed in each language. Each word has its own language, which you can use to split subtitles by language. To set a fixed language, pass language with an ISO 639-1 code (for example, en). This skips detection, including mid-file switching. See pinning the language.
Side by side
From ElevenLabs — Python (elevenlabs_transcribe.py)
from elevenlabs.client import ElevenLabs
client = ElevenLabs() # ELEVENLABS_API_KEY
with open("meeting.mp3", "rb") as f:
resp = client.speech_to_text.convert(
model_id="scribe_v2",
file=f,
diarize=True,
)
print(resp.text)
for w in resp.words:
if w.type == "word": # skip spacing tokens
print(w.speaker_id, w.text, w.start, w.end)To Speech Revolutions — Python (stt_transcribe.py)
from speechrevolutions import SpeechRevolutions
client = SpeechRevolutions() # SPEECHREVOLUTIONS_API_KEY
result = client.transcribe("meeting.mp3", diarize=True) # same flag name
print(result.text)
for w in result.words: # already words only
print(w.speaker, w.text, w.start, w.end)
for u in result.utterances: # pre-grouped speaker turns
print(f"{u.speaker}: {u.text}")From ElevenLabs — JavaScript (elevenlabs-transcribe.ts)
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import { createReadStream } from "node:fs";
const client = new ElevenLabsClient(); // ELEVENLABS_API_KEY
const resp = await client.speechToText.convert({
modelId: "scribe_v2",
file: createReadStream("meeting.mp3"),
diarize: true,
});
console.log(resp.text);
for (const w of resp.words) {
if (w.type === "word") { // skip spacing tokens
console.log(w.speakerId, w.text, w.start, w.end);
}
}To Speech Revolutions — JavaScript (stt-transcribe.ts)
import { SpeechRevolutions } from "speechrevolutions";
const client = new SpeechRevolutions(); // SPEECHREVOLUTIONS_API_KEY
const result = await client.transcribe("meeting.mp3", { diarize: true }); // same flag name
console.log(result.text);
for (const w of result.words) { // already words only
console.log(w.speaker, w.text, w.start, w.end);
}
for (const u of result.utterances) { // pre-grouped speaker turns
console.log(`${u.speaker}: ${u.text}`);
}From ElevenLabs — Go (elevenlabs_transcribe.go)
// ElevenLabs has no official Go SDK, so this calls the REST API directly.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
)
func main() {
audio, err := os.ReadFile("meeting.mp3")
if err != nil {
log.Fatal(err)
}
var body bytes.Buffer
form := multipart.NewWriter(&body)
form.WriteField("model_id", "scribe_v2")
form.WriteField("diarize", "true")
part, _ := form.CreateFormFile("file", "meeting.mp3")
part.Write(audio)
form.Close()
req, _ := http.NewRequest("POST", "https://api.elevenlabs.io/v1/speech-to-text", &body)
req.Header.Set("xi-api-key", os.Getenv("ELEVENLABS_API_KEY"))
req.Header.Set("Content-Type", form.FormDataContentType())
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
log.Fatalf("elevenlabs: %s: %s", res.Status, msg)
}
var resp struct {
Text string `json:"text"`
Words []struct {
Text string `json:"text"`
Start float64 `json:"start"`
End float64 `json:"end"`
Type string `json:"type"`
SpeakerID string `json:"speaker_id"`
} `json:"words"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text)
for _, w := range resp.Words {
if w.Type == "word" { // skip spacing tokens
fmt.Println(w.SpeakerID, w.Text, w.Start, w.End)
}
}
}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)
}
// an official SDK: no multipart form, no response struct of your own
result, err := client.Transcribe(ctx, "meeting.mp3", stt.TranscribeOptions{
Diarize: stt.Bool(true), // same flag name
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text())
for _, w := range result.Words { // already words only
if w.Start != nil && w.End != nil {
fmt.Println(w.Speaker, w.Word, *w.Start, *w.End)
}
}
for _, u := range result.Utterances { // pre-grouped speaker turns
fmt.Printf("%s: %s\n", u.Speaker, u.Text)
}
}From ElevenLabs — C# (Program.cs)
// ElevenLabs has no official .NET SDK, so this calls the REST API directly.
using System.Net.Http.Headers;
using System.Text.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("xi-api-key", Environment.GetEnvironmentVariable("ELEVENLABS_API_KEY"));
using var form = new MultipartFormDataContent
{
{ new StringContent("scribe_v2"), "model_id" },
{ new StringContent("true"), "diarize" },
};
var audio = new ByteArrayContent(await File.ReadAllBytesAsync("meeting.mp3"));
audio.Headers.ContentType = new MediaTypeHeaderValue("audio/mpeg");
form.Add(audio, "file", "meeting.mp3");
var res = await http.PostAsync("https://api.elevenlabs.io/v1/speech-to-text", form);
res.EnsureSuccessStatusCode();
using var json = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
Console.WriteLine(json.RootElement.GetProperty("text").GetString());
foreach (var w in json.RootElement.GetProperty("words").EnumerateArray())
{
if (w.GetProperty("type").GetString() != "word") continue; // skip spacing tokens
Console.WriteLine($"{w.GetProperty("speaker_id")} {w.GetProperty("text")} " +
$"{w.GetProperty("start")} {w.GetProperty("end")}");
}To Speech Revolutions — C# (Program.cs)
using SpeechRevolutions;
// an official SDK: no multipart form, no JSON walking
using var client = new SpeechRevolutionsClient(); // SPEECHREVOLUTIONS_API_KEY
var result = await client.TranscribeAsync(
"meeting.mp3",
new TranscribeOptions { Diarize = true }); // same flag name
Console.WriteLine(result.Text);
foreach (var w in result.Words) // already words only
Console.WriteLine($"{w.Speaker} {w.Text} {w.Start} {w.End}");
foreach (var u in result.Utterances) // pre-grouped speaker turns
Console.WriteLine($"{u.Speaker}: {u.Text}");Common pitfalls
- Auth header.
xi-api-key→X-API-Key. - Spacing tokens. The
wordsarray omits them. Remove anytypefiltering. - Speaker grouping. Use
result.utterancesinstead of regrouping per-wordspeaker_idvalues. - Keyword biasing. Scribe v2 keyterm prompting maps to
custom_vocabulary, a list of domain terms.
See the migration playbook for cutover, and the benchmarks for diarization and timestamp comparisons.