Using Speech Revolutions with Django

Integrate Speech Revolutions into a Django app: a view that submits a file for transcription, a model that stores each job's status and progress, and a webhook view that verifies the signature and records the result. The API key stays in Django settings / the server environment — it never reaches the browser.

Keep the key server-side

SpeechRevolutions() reads SPEECHREVOLUTIONS_API_KEY from the environment. Views run on the server, so the client and key never ship to templates or JavaScript.

Install

bash
pip install django speechrevolutions

export SPEECHREVOLUTIONS_API_KEY=stt_...

A model to store status + progress

Persist the Speech Revolutions job_id plus a status and a 0–100 progress number. Store the download URL and transcript once the job completes.

transcripts/models.py
from django.db import models


class TranscriptionJob(models.Model):
    class Status(models.TextChoices):
        PROCESSING = "processing"
        COMPLETED = "completed"
        FAILED = "failed"

    job_id = models.CharField(max_length=64, unique=True, db_index=True)
    status = models.CharField(
        max_length=16, choices=Status.choices, default=Status.PROCESSING
    )
    percent = models.FloatField(default=0.0)          # overall 0-100
    download_url = models.URLField(blank=True, default="")
    text = models.TextField(blank=True, default="")
    reason = models.CharField(max_length=255, blank=True, default="")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

A view that submits

Take the uploaded file, call submit() to get a job id without blocking the request, and create the row. Pass a callback_url so Speech Revolutions notifies you when the job finishes — the webhook view below fills in the result.

transcripts/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from speechrevolutions import SpeechRevolutions

from .models import TranscriptionJob

client = SpeechRevolutions()  # reads SPEECHREVOLUTIONS_API_KEY


@require_POST
def start_transcription(request):
    upload = request.FILES["file"]

    # submit() returns a job id without holding the request open.
    job_id = client.submit(
        upload.read(),
        speaker_labels=True,
        callback_url=request.build_absolute_uri("/webhooks/stt/"),
    )

    TranscriptionJob.objects.create(job_id=job_id)
    return JsonResponse({"job_id": job_id, "status": "processing"})


def job_progress(request, job_id):
    # get() would raise DoesNotExist and return a 500 for an id that has been
    # mistyped, expired, or never existed. 404 is the honest answer.
    job = get_object_or_404(TranscriptionJob, job_id=job_id)
    return JsonResponse(
        {"status": job.status, "percent": job.percent, "text": job.text}
    )

submit() returns immediately and doesn't stream progress. To move percent between submit and completion, run the blocking transcribe() with on_progress/on_upload_progress callbacks in a background worker (Celery, RQ, or a thread) that writes each update to the row. See Live progress for web apps for the callback-to-bar weighting.

Webhook view with signature verification

Speech Revolutions POSTs a signed JSON body to your callback_url on completion or permanent failure. The signature is HMAC-SHA256 over the raw body in the X-SR-Signature: sha256=<hex> header. Verify against request.body (the exact bytes) and exempt the view from CSRF — it's a server-to-server POST, not a browser form.

transcripts/webhooks.py
import hashlib
import hmac
import json

from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from .models import TranscriptionJob


def verify_signature(raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        settings.STT_WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")


@csrf_exempt
@require_POST
def stt_webhook(request):
    raw = request.body  # verify against the exact bytes received
    if not verify_signature(raw, request.headers.get("X-SR-Signature", "")):
        return HttpResponse(status=401)

    event = json.loads(raw)
    # {job_id, status: "completed" | "failed", download_url?, step?, reason?}
    try:
        job = TranscriptionJob.objects.get(job_id=event["job_id"])
    except TranscriptionJob.DoesNotExist:
        return HttpResponse(status=404)

    if event["status"] == "completed":
        job.status = TranscriptionJob.Status.COMPLETED
        job.percent = 100.0
        job.download_url = event.get("download_url", "")
        # Fetch + parse the transcript, then store the text:
        # from speechrevolutions import SpeechRevolutions
        # job.text = SpeechRevolutions().get_transcript(job.job_id).text
    else:
        job.status = TranscriptionJob.Status.FAILED
        job.reason = event.get("reason", "")
    job.save()

    return JsonResponse({"ok": True})  # a 2xx acks delivery; 5xx is retried

URLs

urls.py
from django.urls import path
from transcripts import views
from transcripts.webhooks import stt_webhook

urlpatterns = [
    path("transcribe/", views.start_transcription),
    path("jobs/<str:job_id>/progress/", views.job_progress),
    path("webhooks/stt/", stt_webhook),
]

submit() creates the job via the upload flow and returns its id; status and the transcript are fetched later through the jobs endpoints. See the Python SDK for the full surface.