Record your voice, get it transcribed, have a language model expand on it, and hear the result read back.
A full-stack demo built with Django REST Framework and React + Vite. It runs from a fresh clone with no database server and no configuration — add an OpenAI key when you want the audio pipeline to do real work.
Browser (MediaRecorder)
│ POST /api/audio_processing/ multipart: file + user_id
▼
Django ──▶ ffmpeg (trim to 60s)
──▶ OpenAI Whisper speech → text
──▶ OpenAI gpt-3.5-turbo text → elaborated text (≤150 words)
──▶ gTTS text → mp3
│
▼ JSON: { audio_file, data_to_show[] }
Browser renders the transcript and speaks it with the Web Speech API
The generated mp3 is stored server-side and its URL returned, but playback in the browser uses the Web Speech API on the response text, not that file.
- Voice recording in the browser via
MediaRecorder. - Transcription with OpenAI Whisper, capped at 60 seconds per clip.
- Text generation with
gpt-3.5-turbo-0125, capped at ~150 words. - Playback with play / pause / resume through the Web Speech API.
- Rolling history of the last 10 exchanges per session, with a pager.
- Contact form protected by Google reCAPTCHA.
- Automatic data expiry — stored recordings and transcripts are swept after 24 hours by default.
| Layer | Choice |
|---|---|
| Backend | Python 3.12, Django 5.2, Django REST Framework 3.16 |
| Frontend | React 19, Vite 7, Tailwind CSS 3 |
| Database | SQLite by default; PostgreSQL/MySQL via DB_ENGINE |
| Speech-to-text | OpenAI Whisper (whisper-1) |
| Text generation | OpenAI (gpt-3.5-turbo-0125) |
| Text-to-speech | gTTS (server-side), Web Speech API (in-browser playback) |
| Audio decoding | pydub + ffmpeg |
| Serving | Gunicorn + WhiteNoise (backend), nginx (frontend) |
- Python 3.12+
- Node.js 22+
- ffmpeg and ffprobe on
PATH— pydub shells out to them to decode uploads.- Debian/Ubuntu:
sudo apt-get install ffmpeg - macOS:
brew install ffmpeg - Windows:
winget install Gyan.FFmpeg, then reopen your terminal
- Debian/Ubuntu:
Without ffmpeg the app starts and the contact form works; the audio endpoint
returns 503 with a message saying so.
git clone <repository-url>
cd voice-geniusBackend (terminal 1):
cd backend
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # runs as-is; set OPENAI_API_KEY for the audio pipeline
python manage.py migrate # creates db.sqlite3
python manage.py runserverFrontend (terminal 2):
cd frontend
npm ci
cp .env.example .env
npm run devOpen http://localhost:5173. The API is at http://localhost:8000/api/.
Builds the production images — Gunicorn and nginx, both running as non-root:
cp backend/.env.example backend/.env # optional; set OPENAI_API_KEY here
docker compose up --buildFrontend on http://localhost:8080, API on http://localhost:8000. Requires
Docker Compose v2.24+ (the .env file is declared optional, so the stack still
comes up without one).
Copy backend/.env.example → backend/.env and frontend/.env.example →
frontend/.env. Every variable below is read by the code; nothing is listed here
that the application does not actually use.
| Variable | Default | Description |
|---|---|---|
DEBUG |
false |
Never enable in production. Also gates the fallbacks below. |
DJANGO_SECRET_KEY |
dev fallback when DEBUG=true |
Required when DEBUG=false — startup fails without it. |
ALLOWED_HOSTS |
localhost,127.0.0.1,[::1] when DEBUG=true |
Required when DEBUG=false. * is rejected. |
CORS_ALLOWED_ORIGINS |
localhost:5173 when DEBUG=true, else empty |
Comma-separated origins allowed to call the API. |
CSRF_TRUSTED_ORIGINS |
same as CORS_ALLOWED_ORIGINS |
Comma-separated. |
OPENAI_API_KEY |
(empty) | Required for the audio pipeline; without it that endpoint returns 503. |
RECAPTCHA_SECRET_KEY |
(empty when DEBUG=true) |
Required when DEBUG=false. Empty in debug skips verification. |
DB_ENGINE |
django.db.backends.sqlite3 |
Point at another backend to use a database server. |
DB_NAME |
backend/db.sqlite3 |
Required when DB_ENGINE is not SQLite. |
DB_USER |
— | Required when DB_ENGINE is not SQLite. |
DB_PASSWORD |
— | Required when DB_ENGINE is not SQLite. |
DB_HOST |
localhost |
Ignored for SQLite. |
DB_PORT |
(empty) | Ignored for SQLite. |
OPENAI_TRANSCRIPTION_MODEL |
whisper-1 |
Speech-to-text model. |
OPENAI_TEXT_MODEL |
gpt-3.5-turbo-0125 |
Text generation model. |
OPENAI_TIMEOUT_SECONDS |
60 |
Per-request timeout for OpenAI calls. |
MEDIA_ROOT |
backend/media |
Where recordings and transcripts are written. |
AUDIO_MAX_DURATION_SECONDS |
60 |
Uploads are trimmed to this before transcription. |
AUDIO_MAX_UPLOAD_BYTES |
10485760 (10 MB) |
Larger uploads are rejected before touching disk. |
AUDIO_HISTORY_LENGTH |
10 |
Transcript/response pairs kept per session. |
AUDIO_RETENTION_HOURS |
24 |
Session folders older than this are swept. 0 disables. |
AUDIO_PERSIST_RECORDINGS |
true |
Set false to never store audio, keeping only text. |
LOG_LEVEL |
INFO |
Root logger level. |
API_ANON_THROTTLE_RATE |
30/minute |
DRF throttle for anonymous callers. |
SECURE_SSL_REDIRECT |
true when DEBUG=false |
Set false if a proxy already redirects. |
SECURE_HSTS_SECONDS |
31536000 |
Applied only when DEBUG=false. |
| Variable | Default | Description |
|---|---|---|
VITE_API_URL |
http://localhost:8000 |
Backend origin only — the /api/ prefix is added in code. |
VITE_RECAPTCHA_SITE_KEY |
(empty) | Public site key. Without it the form shows a notice instead of the captcha. |
Vite inlines
VITE_*variables at build time, not at container start. A Docker image must receive them as build arguments; seedocker-compose.yml. Never put a secret in aVITE_*variable — they ship in the bundle.
Base URL: {VITE_API_URL}/api/. Both endpoints are unauthenticated and subject
to API_ANON_THROTTLE_RATE.
Submits the contact form.
Body (application/json):
| Field | Type | Notes |
|---|---|---|
first_name |
string | max 100 |
last_name |
string | max 100 |
company |
string | max 200 |
email |
string | must be a valid email |
project |
string[] | at least one entry |
message |
string | |
recaptcha_token |
string | verified unless DEBUG=true and no secret key is set |
Responses: 201 {"message": "Form submitted successfully"} · 400 with a
field-keyed error object · 429 when throttled.
Transcribes a recording, elaborates on it, and synthesises speech.
Body (multipart/form-data):
| Field | Type | Notes |
|---|---|---|
file |
file | Non-empty, at most AUDIO_MAX_UPLOAD_BYTES |
user_id |
string | Must be a valid UUID — it names the storage folder |
200:
{
"audio_file": "http://localhost:8000/media/<uuid>/latest_output.mp3",
"data_to_show": [
{ "transcription": "hello world", "generated_text": "..." }
]
}data_to_show is newest-first and holds up to AUDIO_HISTORY_LENGTH entries.
audio_file is null when AUDIO_PERSIST_RECORDINGS=false.
Error responses — each is a real status, never a 200 carrying a failure
message in the transcript field:
| Status | Meaning |
|---|---|
400 |
Invalid user_id, empty/oversized file, or undecodable audio |
429 |
Rate limited |
502 |
Transcription, generation, or speech synthesis failed upstream |
503 |
OPENAI_API_KEY unset, or ffmpeg/ffprobe not on PATH |
Errors are shaped {"error": "..."}; validation errors are field-keyed.
/media/is served by Django only whenDEBUG=true. In production, serve it from your web server or object store.
voice-genius/
├── backend/
│ ├── VoiceGenius/ # Django project: settings, root urls, wsgi/asgi
│ ├── voiceApp/
│ │ ├── views.py # HTTP transport only
│ │ ├── serializers.py # request validation
│ │ ├── services.py # business logic: storage, retention, orchestration
│ │ ├── providers.py # adapters for OpenAI, gTTS, ffmpeg
│ │ ├── exceptions.py # domain errors, each with an HTTP status
│ │ ├── models.py # AppFormSubmission
│ │ └── management/commands/purge_sessions.py
│ ├── tests/ # pytest suite (49 tests)
│ ├── requirements.txt # runtime dependencies
│ ├── requirements-dev.txt # + ruff, pytest, pip-audit
│ └── pyproject.toml # ruff and pytest configuration
├── frontend/
│ └── src/
│ ├── components/ # presentational components
│ ├── hooks/ # stateful logic
│ ├── pages/Home.jsx
│ ├── services/ # API client and URL building
│ ├── data/products.js
│ └── constants.js # shared magic values
├── .github/workflows/ci.yml
├── docker-compose.yml
├── CONTRIBUTING.md
├── SECURITY.md
└── LICENSE
The exact commands CI runs are in CONTRIBUTING.md.
# Backend, from backend/
ruff check . && ruff format --check . && pytest
# Frontend, from frontend/
npm run lint && npm test && npm run buildDelete stored personal data on demand:
python manage.py purge_sessions # anything past AUDIO_RETENTION_HOURS
python manage.py purge_sessions --all # everythingWorth knowing before you build on this:
- Both API endpoints are unauthenticated. Rate limiting and reCAPTCHA are the only protection. Anyone who can reach the audio endpoint spends your OpenAI quota.
- The session id is client-supplied. It is validated as a UUID so it cannot
escape
MEDIA_ROOT, but anyone holding another session's UUID can read that session's transcript history. It is not an authentication mechanism. - Recordings are capped at 60 seconds; anything longer is silently trimmed.
- English only. Transcription, generation and synthesis are all hardcoded to
en. - Contact form submissions never expire automatically. Audio and transcripts do; database rows do not. See SECURITY.md.
- Recordings leave your infrastructure. Audio goes to OpenAI; generated text goes to Google's TTS endpoint via gTTS. Disclose this to your users.
- No streaming. The request blocks for the full transcribe → generate → synthesise round trip, typically several seconds.
gpt-3.5-turbo-0125is a dated model. ChangeOPENAI_TEXT_MODELto use a current one.- Browser support:
MediaRecorderand the Web Speech API are required. Playback quality varies considerably between browsers.
Please report vulnerabilities privately — see SECURITY.md, which also carries a safe-deployment checklist and a description of exactly what personal data this application collects.
Distributed under the GNU General Public License v3.0. See LICENSE.
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.