Migrating from self-hosted Whisper to Speech Revolutions
This guide covers moving from self-hosted Whisper (faster-whisper with CTranslate2 on a GPU, or whisper.cpp on CPU/Metal) to the Speech Revolutions API. Self-hosting means provisioning GPUs, pinning CUDA/cuDNN, sizing VRAM for large-v3, warming models, batching, autoscaling, and running a separate diarization stack. Speech Revolutions runs Zephyr, its speech-to-text engine, as a hosted API and returns word timestamps and diarization in one response, with a diarization error rate that ranks #1 on every benchmark subset.
GPU capacity, driver and toolkit versions, VRAM limits, cold starts, concurrency and queueing, and a diarization pipeline (neither faster-whisper nor whisper.cpp includes one). With Speech Revolutions, you call the API instead.
Authentication
A local model needs no authentication. Speech Revolutions requires an API key, which the SDK reads from the environment.
export SPEECHREVOLUTIONS_API_KEY=stt_...From a function call to an API call
Self-hosted Whisper runs in process: you load a model into GPU memory once, then call .transcribe() on it. With Speech Revolutions, the SDK uploads the file and waits for the result. The call site is still a single line.
| Self-hosted | Speech Revolutions |
|---|---|
WhisperModel("large-v3", device="cuda") (loads weights into VRAM) | SpeechRevolutions() (reads the API key) |
model.transcribe(path, ...) (runs on your GPU) | client.transcribe(path, ...) (runs on Speech Revolutions) |
lazy segments generator you must iterate | a materialized result: .text, .words, .utterances |
| you operate the GPU, queue, and scaling | hosted; nothing to operate |
Input and upload differences
faster-whisper reads a local file path directly. whisper.cpp requires 16 kHz mono WAV, so most pipelines convert with ffmpeg first. Speech Revolutions accepts common audio and video formats and transcodes them for you. Pass a path, URL, or bytes to the SDK and skip the conversion step.
Response shape
faster-whisper yields Segment objects, each with a words list (start, end, word) when word_timestamps=True, plus an info with the detected language. Speech Revolutions returns one complete result object.
| faster-whisper | Speech Revolutions |
|---|---|
join segment.text across the generator | result.text (already assembled) |
segment.words[] (word, start, end) | result.words (text, start, end, speaker) |
| — (no speaker labels) | result.utterances (speaker turns) |
info.language | result.languages |
faster-whisper's segments is a lazy generator: transcription runs only as you iterate it. Speech Revolutions returns a finished result, so there is no generator to consume.
Diarization
Neither faster-whisper nor whisper.cpp diarizes on its own. You run a separate pipeline (typically pyannote.audio) and align its speaker turns to the Whisper words yourself. 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
With faster-whisper, word timestamps require word_timestamps=True, which adds alignment cost. On Speech Revolutions, word_timestamps is on by default and the times are on result.words in seconds.
Language selection
faster-whisper auto-detects, or you pass language= to transcribe(). Speech Revolutions auto-detects by default, with no per-language model choice, and handles code-switching without splitting the file. The detected spans are returned in result.languages. To set a fixed language, pass the same language= option with an ISO 639-1 code; see pinning the language.
Side by side
From self-hosted Whisper — Python (faster_whisper_transcribe.py)
from faster_whisper import WhisperModel
# loads weights into GPU memory; you own the box, drivers, and VRAM
model = WhisperModel("large-v3", device="cuda", compute_type="float16")
segments, info = model.transcribe("meeting.mp3", word_timestamps=True)
# 'segments' is a lazy generator — transcription runs as you iterate
text = []
for segment in segments:
text.append(segment.text)
for w in segment.words:
print(w.start, w.end, w.word)
print("".join(text))
print("language:", info.language)
# diarization? bring your own pyannote pipeline and align it yourself.To Speech Revolutions — Python (stt_transcribe.py)
from speechrevolutions import SpeechRevolutions
client = SpeechRevolutions() # SPEECHREVOLUTIONS_API_KEY — no GPU, no model files
result = client.transcribe("meeting.mp3", speaker_labels=True)
print(result.text) # already assembled
for w in result.words: # word timestamps, in seconds
print(w.start, w.end, w.text)
for u in result.utterances: # diarization built in
print(f"{u.speaker}: {u.text}")
print("languages:", result.languages) # [{start, end, language}, ...]From self-hosted Whisper — JavaScript (whisper-server.ts)
import { readFileSync } from "node:fs";
// your whisper.cpp server: ./build/bin/whisper-server -m models/ggml-large-v3.bin
const form = new FormData();
form.append("file", new Blob([readFileSync("meeting.wav")]), "meeting.wav"); // 16 kHz mono WAV
form.append("response_format", "json");
const res = await fetch("http://localhost:8080/inference", { method: "POST", body: form });
const { text } = await res.json();
console.log(text);
// no word timestamps in this format, no diarization: bring your own and align it yourself.To Speech Revolutions — JavaScript (stt-transcribe.ts)
import { SpeechRevolutions } from "speechrevolutions";
// no GPU, no model files, no server to keep up
const client = new SpeechRevolutions(); // SPEECHREVOLUTIONS_API_KEY
const result = await client.transcribe("meeting.mp3", { speakerLabels: true }); // MP3 as-is
console.log(result.text); // already assembled
for (const w of result.words) { // word timestamps, in seconds
console.log(w.start, w.end, w.text);
}
for (const u of result.utterances) { // diarization built in
console.log(`${u.speaker}: ${u.text}`);
}
console.log("languages:", result.languages); // [{ start, end, language }, ...]From self-hosted Whisper — Go (whisper_server.go)
// Calls your own whisper.cpp server (whisper-server -m models/ggml-large-v3.bin).
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"mime/multipart"
"net/http"
"os"
)
func main() {
audio, err := os.ReadFile("meeting.wav") // 16 kHz mono WAV
if err != nil {
log.Fatal(err)
}
var body bytes.Buffer
form := multipart.NewWriter(&body)
form.WriteField("response_format", "json")
part, _ := form.CreateFormFile("file", "meeting.wav")
part.Write(audio)
form.Close()
// you run this box: the GPU, the drivers, the model files, the restarts
res, err := http.Post("http://localhost:8080/inference", form.FormDataContentType(), &body)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var resp struct {
Text string `json:"text"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text)
// no word timestamps in this format, no diarization: bring your own and align it yourself.
}To Speech Revolutions — Go (stt_transcribe.go)
package main
import (
"context"
"fmt"
"log"
stt "github.com/speechrevolutions/speechrevolutions-go"
)
func main() {
ctx := context.Background()
// no GPU, no model files, no server to keep up
client, err := stt.NewClient("") // SPEECHREVOLUTIONS_API_KEY
if err != nil {
log.Fatal(err)
}
result, err := client.Transcribe(ctx, "meeting.mp3", stt.TranscribeOptions{ // MP3 as-is
SpeakerLabels: stt.Bool(true),
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text()) // already assembled
for _, w := range result.Words { // word timestamps, in seconds
if w.Start != nil && w.End != nil {
fmt.Println(*w.Start, *w.End, w.Word)
}
}
for _, u := range result.Utterances { // diarization built in
fmt.Printf("%s: %s\n", u.Speaker, u.Text)
}
for _, l := range result.Languages { // language spans
fmt.Println(l.Start, l.End, l.Language)
}
}From self-hosted Whisper — C# (Program.cs)
using Whisper.net;
// loads weights into memory; you own the box, the runtime package and the model file
using var factory = WhisperFactory.FromPath("ggml-large-v3.bin");
using var processor = factory.CreateBuilder()
.WithLanguage("auto")
.Build();
// Whisper.net reads 16 kHz WAV, so convert with ffmpeg first
using var wav = File.OpenRead("meeting.wav");
await foreach (var segment in processor.ProcessAsync(wav))
Console.WriteLine($"{segment.Start}->{segment.End}: {segment.Text}");
// diarization? bring your own pipeline and align it yourself.To Speech Revolutions — C# (Program.cs)
using SpeechRevolutions;
// no GPU, no model files, no runtime package
using var client = new SpeechRevolutionsClient(); // SPEECHREVOLUTIONS_API_KEY
var result = await client.TranscribeAsync(
"meeting.mp3", // MP3 as-is, no WAV conversion
new TranscribeOptions { SpeakerLabels = true });
Console.WriteLine(result.Text); // already assembled
foreach (var w in result.Words) // word timestamps, in seconds
Console.WriteLine($"{w.Start} {w.End} {w.Text}");
foreach (var u in result.Utterances) // diarization built in
Console.WriteLine($"{u.Speaker}: {u.Text}");
foreach (var l in result.Languages) // language spans
Console.WriteLine($"{l.Start} {l.End} {l.Language}");If you call the whisper.cpp binary directly instead of its server, this is the step the code above replaces, along with the WAV conversion:
# convert to 16 kHz mono WAV first (whisper.cpp requirement)
ffmpeg -i meeting.mp3 -ar 16000 -ac 1 -c:a pcm_s16le meeting.wav
# run inference on the compiled binary; you manage the model files + build
./main -m models/ggml-large-v3.bin -f meeting.wav \
--output-srt --max-len 1
# no built-in diarizationWhat you stop maintaining
- GPU fleet. No instances to provision, right-size, or pay for while idle.
- Toolkit versions. No CUDA/cuDNN/CTranslate2 drift or rebuilds.
- Cold starts and batching. No model warming or throughput tuning.
- Diarization stack. No separate
pyannotepipeline and word-alignment code. - Audio preprocessing. No
ffmpegconversion to 16 kHz WAV (required bywhisper.cpp).
Common pitfalls
- Handling the lazy generator. Code that assumed
faster-whisper's lazysegmentsshould read the finishedresultfields directly. - Concurrency model. You don't need to serialize work behind a single GPU. Issue calls concurrently with
submit()or the async client. - Preprocessing assumptions. Drop the forced 16 kHz mono WAV conversion; send the original file.
See the migration playbook for cutover, the Python SDK for concurrency and async, and the benchmarks for accuracy and diarization compared with self-hosted Whisper.