diff --git a/examples/220-django-channels-live-stt-python/.env.example b/examples/220-django-channels-live-stt-python/.env.example new file mode 100644 index 0000000..99314a3 --- /dev/null +++ b/examples/220-django-channels-live-stt-python/.env.example @@ -0,0 +1,2 @@ +# Deepgram — https://console.deepgram.com/ +DEEPGRAM_API_KEY= diff --git a/examples/220-django-channels-live-stt-python/README.md b/examples/220-django-channels-live-stt-python/README.md new file mode 100644 index 0000000..58d1554 --- /dev/null +++ b/examples/220-django-channels-live-stt-python/README.md @@ -0,0 +1,86 @@ +# Django Channels Real-Time Transcription with Deepgram Live STT + +Build a Django 5 application that captures browser microphone audio and streams it through Django Channels WebSockets to Deepgram's Live STT API, displaying transcription results on the page in real-time. The Deepgram API key stays server-side — the browser never sees it. + +## What you'll build + +A Django web application that uses Django Channels to handle WebSocket connections from the browser. When a user clicks "Start Listening", the page captures microphone audio, streams it over a WebSocket to a Django Channels consumer, which forwards it to Deepgram's Live STT API (Nova-3). Transcription results flow back through the same WebSocket and appear on the page instantly. + +## Prerequisites + +- Python 3.11+ +- Deepgram account — [get a free API key](https://console.deepgram.com/) + +## Environment variables + +| Variable | Where to find it | +|----------|-----------------| +| `DEEPGRAM_API_KEY` | [Deepgram console](https://console.deepgram.com/) | + +Copy `.env.example` to `.env` and fill in your values. + +## Install and run + +```bash +cd examples/220-django-channels-live-stt-python + +pip install -r requirements.txt + +cp .env.example .env +# Edit .env and add your DEEPGRAM_API_KEY + +python src/manage.py runserver +``` + +Then open http://127.0.0.1:8000 in your browser and click **Start Listening**. + +## Key parameters + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `model` | `nova-3` | Deepgram's latest and most accurate speech recognition model | +| `smart_format` | `True` | Adds punctuation, capitalization, and number formatting | +| `interim_results` | `True` | Returns partial transcripts while you're still speaking | +| `encoding` | `linear16` | Raw 16-bit PCM audio format from the browser | +| `sample_rate` | `16000` | 16 kHz sample rate — good balance of quality and bandwidth | + +## How it works + +1. **Browser** — the HTML page uses `getUserMedia` to capture microphone audio, resamples it to 16 kHz linear16 PCM via a `ScriptProcessorNode`, and sends binary frames over a WebSocket to `/ws/transcribe/` +2. **Django Channels consumer** (`consumer.py`) — an `AsyncWebsocketConsumer` that on connect opens a Deepgram live STT WebSocket using the official Python SDK (`client.listen.v1.connect()`) +3. **Audio forwarding** — each binary WebSocket frame from the browser is forwarded to Deepgram via `connection.send_media()` +4. **Transcript delivery** — Deepgram fires `EventType.MESSAGE` callbacks with `ListenV1Results`; the consumer sends each transcript back to the browser as JSON with `is_final` indicating whether the result is finalized +5. **Browser display** — interim results appear greyed out and get replaced; final results are appended permanently + +## Architecture + +``` +Browser Microphone + | + | WebSocket (binary PCM audio) + v +Django Channels Consumer + | + | Deepgram Python SDK (WebSocket) + v +Deepgram Live STT (nova-3) + | + | transcript JSON + v +Django Channels Consumer + | + | WebSocket (JSON) + v +Browser Display +``` + +## Related + +- [Deepgram Live STT docs](https://developers.deepgram.com/docs/getting-started-with-live-streaming-audio) +- [Deepgram Python SDK](https://github.com/deepgram/deepgram-python-sdk) +- [Django Channels documentation](https://channels.readthedocs.io/) +- [Daphne ASGI server](https://github.com/django/daphne) + +## Starter templates + +If you want a ready-to-run base for your own project, check the [deepgram-starters](https://github.com/orgs/deepgram-starters/repositories) org — there are starter repos for every language and every Deepgram product. diff --git a/examples/220-django-channels-live-stt-python/requirements.txt b/examples/220-django-channels-live-stt-python/requirements.txt new file mode 100644 index 0000000..a47930e --- /dev/null +++ b/examples/220-django-channels-live-stt-python/requirements.txt @@ -0,0 +1,5 @@ +django>=5.0,<6.0 +channels>=4.0,<5.0 +daphne>=4.0,<5.0 +deepgram-sdk>=4.0.0 +python-dotenv>=1.0.0 diff --git a/examples/220-django-channels-live-stt-python/src/asgi.py b/examples/220-django-channels-live-stt-python/src/asgi.py new file mode 100644 index 0000000..8e93ab8 --- /dev/null +++ b/examples/220-django-channels-live-stt-python/src/asgi.py @@ -0,0 +1,16 @@ +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application + +import urls + +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack(URLRouter(urls.websocket_urlpatterns)), + } +) diff --git a/examples/220-django-channels-live-stt-python/src/consumer.py b/examples/220-django-channels-live-stt-python/src/consumer.py new file mode 100644 index 0000000..6bdfa7a --- /dev/null +++ b/examples/220-django-channels-live-stt-python/src/consumer.py @@ -0,0 +1,75 @@ +"""Django Channels WebSocket consumer that bridges browser audio to Deepgram Live STT. + +Audio flows: browser microphone -> Django Channels WebSocket -> Deepgram Live STT -> transcript back to browser. +The DEEPGRAM_API_KEY stays server-side — the browser never sees it. +""" + +import asyncio +import json +import os + +from channels.generic.websocket import AsyncWebsocketConsumer +from deepgram import AsyncDeepgramClient +from deepgram.core.events import EventType +from deepgram.listen.v1.types import ListenV1Results + + +class TranscriptionConsumer(AsyncWebsocketConsumer): + """Receives raw audio bytes from the browser, streams them to Deepgram, and + sends transcription results back as JSON messages.""" + + async def connect(self): + await self.accept() + self._dg_client = AsyncDeepgramClient( + api_key=os.environ["DEEPGRAM_API_KEY"] + ) + # ← connect() returns a live WebSocket connection to Deepgram's STT API + self._dg_connection = await self._dg_client.listen.v1.connect( + model="nova-3", + smart_format=True, + interim_results=True, + encoding="linear16", + sample_rate=16000, + channels=1, + ) + + async def on_message(message) -> None: + if isinstance(message, ListenV1Results): + # message.channel.alternatives[0].transcript — the transcribed text + transcript = message.channel.alternatives[0].transcript + if transcript.strip(): + await self.send( + text_data=json.dumps( + { + "transcript": transcript, + "is_final": message.is_final, + } + ) + ) + + async def on_error(error) -> None: + await self.send( + text_data=json.dumps({"error": str(error)}) + ) + + self._dg_connection.on(EventType.MESSAGE, on_message) + self._dg_connection.on(EventType.ERROR, on_error) + + # Runs the Deepgram receive loop in the background so events dispatch + self._listener_task = asyncio.create_task( + self._dg_connection.start_listening() + ) + + async def disconnect(self, close_code): + if hasattr(self, "_dg_connection"): + try: + await self._dg_connection.send_close_stream() + except Exception: + pass + if hasattr(self, "_listener_task"): + self._listener_task.cancel() + + async def receive(self, text_data=None, bytes_data=None): + # Browser sends raw PCM audio as binary WebSocket frames + if bytes_data: + await self._dg_connection.send_media(bytes_data) diff --git a/examples/220-django-channels-live-stt-python/src/manage.py b/examples/220-django-channels-live-stt-python/src/manage.py new file mode 100644 index 0000000..9a584c9 --- /dev/null +++ b/examples/220-django-channels-live-stt-python/src/manage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import os +import sys + +from dotenv import load_dotenv + +load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env")) + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/examples/220-django-channels-live-stt-python/src/settings.py b/examples/220-django-channels-live-stt-python/src/settings.py new file mode 100644 index 0000000..fa32e36 --- /dev/null +++ b/examples/220-django-channels-live-stt-python/src/settings.py @@ -0,0 +1,41 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent + +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-insecure-key") + +DEBUG = True + +ALLOWED_HOSTS = ["*"] + +INSTALLED_APPS = [ + "daphne", + "django.contrib.staticfiles", +] + +ROOT_URLCONF = "urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": False, + "OPTIONS": { + "context_processors": [], + }, + }, +] + +ASGI_APPLICATION = "asgi.application" + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer", + }, +} + +STATIC_URL = "static/" +STATICFILES_DIRS = [] + +DATABASES = {} diff --git a/examples/220-django-channels-live-stt-python/src/templates/index.html b/examples/220-django-channels-live-stt-python/src/templates/index.html new file mode 100644 index 0000000..b95c50f --- /dev/null +++ b/examples/220-django-channels-live-stt-python/src/templates/index.html @@ -0,0 +1,133 @@ + + +
+ + +Powered by Django Channels + Deepgram Nova-3
+ +