Migrating from AssemblyAI to Speech Revolutions
AssemblyAI already uses an async flow: upload the file to /v2/upload, submit a job to /v2/transcript, then poll /v2/transcript/{id} until status === "completed". Speech Revolutions follows the same async model. The SDK runs all three steps in one blocking transcribe() call, or you can keep them separate with submit() and get_transcript().
result.text and result.utterances map directly to AssemblyAI's text and utterances. The SDK accepts options as keyword arguments or as a TranscribeOptions config object, like AssemblyAI's TranscriptionConfig.
Authentication
AssemblyAI sends the key in a bare authorization header (no Bearer prefix). Speech Revolutions uses X-API-Key, read from the environment by the SDK.
| AssemblyAI | Speech Revolutions |
|---|---|
authorization: ASSEMBLYAI_API_KEY | X-API-Key: SPEECHREVOLUTIONS_API_KEY |
ASSEMBLYAI_API_KEY env var | SPEECHREVOLUTIONS_API_KEY |
Endpoint & method mapping
| AssemblyAI | Speech Revolutions REST | Speech Revolutions SDK |
|---|---|---|
POST /v2/upload (raw bytes → upload_url) | POST /api/v1/transcribe (raw bytes in, transcript streamed back; up to 200 MB) | submit() (or transcribe() to also wait) |
POST /v2/transcript (audio_url → job id) | ||
GET /v2/transcript/{id} (poll until completed) | GET /api/v1/jobs/{id} or /stream (SSE) | get_job_status() / get_transcript() |
AssemblyAI uploads first, returns an upload_url, then creates a transcript from it. In a Speech Revolutions SDK this is one call: pass a path or URL to transcribe() and it uploads the file, waits, and returns the transcript. To test from a terminal, /api/v1/transcribe does the same in one request.
Upload differences
Both platforms accept a hosted URL. If you already pass an audio_url that points at your own storage, pass the same URL to transcribe(). For local files, AssemblyAI streams bytes to /v2/upload; Speech Revolutions streams them to a presigned object-storage URL. Speech Revolutions also reports live upload and transcribe progress that you can render as a progress bar instead of polling a status field (see live progress).
Response shape
AssemblyAI returns a flat object with text and a words[] array, with timestamps in milliseconds. Speech Revolutions returns timestamps in seconds.
| AssemblyAI field | Speech Revolutions |
|---|---|
text | result.text |
words[] (text, start/end in ms, speaker) | result.words (text, start/end in seconds, speaker) |
utterances[] (speaker turns) | result.utterances |
language_code | result.languages |
AssemblyAI timestamps are in milliseconds; Speech Revolutions timestamps are in seconds. If your code divides by 1000, remove that step.
Diarization
AssemblyAI enables diarization with speaker_labels: true. Speech Revolutions uses the same speaker_labels flag (on by default) and returns utterances in the same way, so speaker-turn code ports with few changes. Zephyr ranks #1 on diarization error rate on every benchmark subset: 9.7% on AMI, against 25.3% for AssemblyAI. See the benchmarks and the comparison table.
Timestamps
Both return word timestamps. The only change is the unit (milliseconds → seconds). For timestamp precision, see the benchmarks.
Language selection
AssemblyAI takes language_code, or language_detection: true to auto-detect. Speech Revolutions auto-detects by default, so remove language_detection from your request. Detection runs per segment, so result.languages reports language spans rather than one language for the whole file. To set a fixed language, replace language_code with language and an ISO 639-1 code (for example, en). This skips detection for the whole file; see pinning the language.
Side by side
From AssemblyAI — Python (assemblyai_transcribe.py)
import assemblyai as aai
aai.settings.api_key = "..." # ASSEMBLYAI_API_KEY
transcriber = aai.Transcriber()
config = aai.TranscriptionConfig(speaker_labels=True)
transcript = transcriber.transcribe("meeting.mp3", config) # uploads + polls
print(transcript.text)
for u in transcript.utterances:
print(f"{u.speaker}: {u.text}")
for w in transcript.words:
print(w.text, w.start, w.end) # start/end in millisecondsTo Speech Revolutions — Python (stt_transcribe.py)
from speechrevolutions import SpeechRevolutions, TranscribeOptions
client = SpeechRevolutions() # SPEECHREVOLUTIONS_API_KEY
# config-object style, like AssemblyAI's TranscriptionConfig
result = client.transcribe(
"meeting.mp3",
options=TranscribeOptions(speaker_labels=True),
)
print(result.text)
for u in result.utterances:
print(f"{u.speaker}: {u.text}")
for w in result.words:
print(w.text, w.start, w.end) # start/end in secondsFrom AssemblyAI — JavaScript (assemblyai-transcribe.ts)
import { AssemblyAI } from "assemblyai";
const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY! });
// uploads + polls
const transcript = await client.transcripts.transcribe({
audio: "meeting.mp3",
speaker_labels: true,
});
console.log(transcript.text);
for (const u of transcript.utterances ?? []) {
console.log(`${u.speaker}: ${u.text}`);
}
for (const w of transcript.words ?? []) {
console.log(w.text, w.start, w.end); // start/end in milliseconds
}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", { speakerLabels: true });
console.log(result.text);
for (const u of result.utterances) {
console.log(`${u.speaker}: ${u.text}`);
}
for (const w of result.words) {
console.log(w.text, w.start, w.end); // start/end in seconds
}From AssemblyAI — Go (assemblyai_transcribe.go)
package main
import (
"context"
"fmt"
"log"
"os"
aai "github.com/AssemblyAI/assemblyai-go-sdk"
)
func main() {
ctx := context.Background()
client := aai.NewClient(os.Getenv("ASSEMBLYAI_API_KEY"))
f, err := os.Open("meeting.mp3")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// uploads + polls
transcript, err := client.Transcripts.TranscribeFromReader(ctx, f, &aai.TranscriptOptionalParams{
SpeakerLabels: aai.Bool(true),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(aai.ToString(transcript.Text))
for _, u := range transcript.Utterances {
fmt.Printf("%s: %s\n", aai.ToString(u.Speaker), aai.ToString(u.Text))
}
for _, w := range transcript.Words {
fmt.Println(aai.ToString(w.Text), aai.ToInt64(w.Start), aai.ToInt64(w.End)) // milliseconds
}
}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)
}
// uploads + waits, like TranscribeFromReader
result, err := client.Transcribe(ctx, "meeting.mp3", stt.TranscribeOptions{
SpeakerLabels: stt.Bool(true),
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text())
for _, u := range result.Utterances {
fmt.Printf("%s: %s\n", u.Speaker, u.Text)
}
for _, w := range result.Words {
if w.Start != nil && w.End != nil {
fmt.Println(w.Word, *w.Start, *w.End) // start/end in seconds
}
}
}From AssemblyAI — C# (Program.cs)
using AssemblyAI;
using AssemblyAI.Transcripts;
var client = new AssemblyAIClient(Environment.GetEnvironmentVariable("ASSEMBLYAI_API_KEY")!);
// uploads + polls
var transcript = await client.Transcripts.TranscribeAsync(
new FileInfo("meeting.mp3"),
new TranscriptOptionalParams { SpeakerLabels = true });
transcript.EnsureStatusCompleted();
Console.WriteLine(transcript.Text);
foreach (var u in transcript.Utterances!)
Console.WriteLine($"{u.Speaker}: {u.Text}");
foreach (var w in transcript.Words!)
Console.WriteLine($"{w.Text} {w.Start} {w.End}"); // start/end in millisecondsTo Speech Revolutions — C# (Program.cs)
using SpeechRevolutions;
using var client = new SpeechRevolutionsClient(); // SPEECHREVOLUTIONS_API_KEY
// uploads + waits; options object, like TranscriptOptionalParams
var result = await client.TranscribeAsync(
"meeting.mp3",
new TranscribeOptions { SpeakerLabels = true });
Console.WriteLine(result.Text);
foreach (var u in result.Utterances)
Console.WriteLine($"{u.Speaker}: {u.Text}");
foreach (var w in result.Words)
Console.WriteLine($"{w.Text} {w.Start} {w.End}"); // start/end in secondsNon-blocking, if you polled before
If your AssemblyAI code submits and polls on its own schedule (for example, from a worker), use submit() and get_job_status() instead of the blocking transcribe():
job_id = client.submit("meeting.mp3", speaker_labels=True)
status = client.get_job_status(job_id)
if status.is_completed:
result = client.get_transcript(job_id)
print(result.text)To skip polling, set a callback_url webhook. Speech Revolutions sends a signed POST when the job finishes.
Common pitfalls
- Timestamp units. Milliseconds → seconds. This is the most common bug after migrating.
- Auth header name.
authorization→X-API-Key. - Upload flow. AssemblyAI uploads, then references the upload URL in a second request. The Speech Revolutions SDK does both in one
transcribe()call. - Keyword biasing. AssemblyAI's
keyterms_promptand the deprecatedword_boostboth map tocustom_vocabulary.
See the migration playbook for cutover, and the benchmarks for accuracy, diarization, and timestamp precision.