# How to Add Audio to Text Conversion to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Audio-to-text conversion needs a microphone input layer (MediaRecorder API), a transcription engine (OpenAI Whisper API at $0.006/minute or Deepgram Nova-2 at $0.0043/minute for streaming), a Supabase Edge Function to proxy the API call, and a transcript display with inline editing. With Lovable or V0 you can ship both a record tab and a file upload tab in 4–8 hours. Running costs start near zero and scale linearly with audio minutes — 100 users transcribing 10 minutes/month each costs roughly $6–15/mo.

## What Audio-to-Text Conversion Actually Is

Audio-to-text conversion turns spoken audio into editable, searchable text. There are two modes: live microphone transcription where words appear on screen as the user speaks, and batch file transcription where the user uploads an MP3 or WAV and gets a full transcript back in seconds. The transcription engine is the most important decision — Web Speech API is free but Chrome-only and English-heavy; OpenAI Whisper handles 99 languages and files up to 25 MB at $0.006/minute; Deepgram Nova-2 adds real-time streaming and speaker diarization at similar cost. The real product complexity is what happens after transcription: speaker labels, editable text, transcript history, and export formats.

## Anatomy of the Feature

Six components. Three are UI, one is a cloud API service, one is a Supabase Edge Function proxy, and one is the storage and history layer. The Edge Function is the critical piece — calling Whisper from the browser directly triggers CORS errors and exposes your API key.

- **Microphone input layer** (ui): MediaDevices.getUserMedia({ audio: true }) requests microphone access. MediaRecorder captures audio chunks as audio/webm (Opus codec). On stop, the Blob is assembled from chunks and sent to the transcription Edge Function. A Web Audio API AnalyserNode drives the waveform visualizer by reading frequency data on each animation frame.
- **Transcription engine** (service): OpenAI Whisper API (model: whisper-1) processes audio files up to 25 MB in 99 languages. Send with response_format: verbose_json to receive word-level timestamps alongside the transcript text. Deepgram Nova-2 is the production-grade alternative: $0.0043/minute for streaming real-time transcription with speaker diarization via a WebSocket connection.
- **Audio file upload** (ui): react-dropzone (^14.x) handles drag-and-drop with audio/* MIME type filtering. Client-side validation checks file size before upload (Whisper max: 25 MB). The file is uploaded to Supabase Storage under the user's folder, then the storage URL is passed to the Edge Function rather than sending the raw file — avoids doubled upload time for large files.
- **Transcript display** (ui): Transcript text rendered in an editable textarea with paragraph breaks preserved. Speaker labels from diarization displayed as colored chips above each speaker's paragraph. Confidence-based word opacity: if Whisper verbose_json includes word-level confidence (approximated from segment avg_logprob), lower-confidence words render at slightly reduced opacity to flag them for review.
- **Supabase Edge Function proxy** (backend): A Deno-based Supabase Edge Function receives the audio Blob or storage URL, authenticates the caller via the Supabase JWT, calls the Whisper API with the OPENAI_API_KEY from Deno.env.get(), and returns the verbose_json response. For file transcription, the function fetches the file from Supabase Storage using the service role key.
- **Storage and history layer** (data): Supabase table transcriptions stores user_id, audio_url, transcript_text, transcript_json (the full verbose_json), language, duration_seconds, speaker_count, and status. RLS ensures users only see their own transcripts. A history page lists past transcriptions with title, date, duration, and a link to re-open and edit.

## Data model

One table with a status lifecycle covers the transcription pipeline. Run this in the Supabase SQL editor.

```sql
create table public.transcriptions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  title text,
  audio_url text,
  transcript_text text,
  transcript_json jsonb,
  language text not null default 'en',
  duration_seconds int,
  speaker_count int,
  status text not null default 'pending'
    check (status in ('pending', 'processing', 'done', 'error')),
  created_at timestamptz not null default now()
);

alter table public.transcriptions enable row level security;

create policy "Users can view own transcriptions"
  on public.transcriptions for select
  using (auth.uid() = user_id);

create policy "Users can insert own transcriptions"
  on public.transcriptions for insert
  with check (auth.uid() = user_id);

create policy "Users can update own transcriptions"
  on public.transcriptions for update
  using (auth.uid() = user_id);

create policy "Users can delete own transcriptions"
  on public.transcriptions for delete
  using (auth.uid() = user_id);

create index transcriptions_user_recent_idx
  on public.transcriptions (user_id, created_at desc);
```

Set status to 'processing' immediately on upload, then update to 'done' or 'error' when the Edge Function returns. This lets you show a loading state in the history list for in-progress transcriptions without polling the Edge Function.

## Build paths

### Lovable — fit 4/10, 4–7 hours

Lovable handles the file upload flow, Supabase Storage wiring, and transcript display UI cleanly. The Edge Function calling Whisper needs a single focused prompt specifying the OpenAI endpoint, request format, and response handling.

1. Create a new Lovable project and connect Lovable Cloud to auto-provision Supabase auth and storage
2. Run the SQL schema above in the Supabase SQL editor via the Cloud tab
3. Paste the prompt below; let Agent Mode build the two-tab transcription page, Edge Function, and history screen
4. In the Cloud tab → Secrets, add OPENAI_API_KEY — Lovable injects this into the Edge Function automatically
5. Click Publish and open the published URL on HTTPS to test microphone access — the Lovable preview iframe blocks getUserMedia

Starter prompt:

```
Build a transcription app with two tabs: 'Record' and 'Upload'. In the Record tab: request microphone permission with MediaDevices.getUserMedia({ audio: true }); show a live waveform visualizer using Web Audio API AnalyserNode drawing bars on a canvas element; record audio with MediaRecorder using audio/webm MIME type; show a 'Stop & Transcribe' button that stops recording and sends the audio Blob to a Supabase Edge Function named 'transcribe'; show a processing spinner while waiting. In the Upload tab: use react-dropzone accepting audio/* files up to 25 MB; show file name and size on drop; upload to Supabase Storage under user-id/filename; send the storage public URL to the same 'transcribe' Edge Function; show file-too-large error if file exceeds 25 MB. The Edge Function calls OpenAI Whisper API at https://api.openai.com/v1/audio/transcriptions with model: whisper-1 and response_format: verbose_json; reads OPENAI_API_KEY from Deno.env.get; returns the transcript text and segments array. On success: render the transcript in an editable textarea with paragraph breaks; show speaker labels if segments include speaker fields; add an Export button that downloads the text as a .txt file using Blob + URL.createObjectURL. Save each transcription to a Supabase transcriptions table with user_id, title (first 60 chars of transcript), audio_url, transcript_text, transcript_json, language, duration_seconds, status. Show a history tab listing past transcriptions newest first with title, date, duration, and delete button. Handle states: idle / requesting-permission / permission-denied / recording / processing / done / error. Handle file-too-large with a toast error.
```

Limitations:

- Microphone access (getUserMedia) requires the published URL — it does not work inside Lovable's preview iframe; always test on the deployed app
- Lovable's Edge Function generation sometimes omits the multipart/form-data encoding Whisper requires for binary audio blobs — if the Edge Function returns a 400, prompt 'fix the Edge Function to send audio as multipart/form-data with field name file'
- Web Speech API real-time streaming (words appearing as you speak) is not what Lovable generates by default; it generates Whisper batch mode. For true real-time display, explicitly prompt for Web Speech API as the live preview layer

### V0 — fit 4/10, 5–8 hours

V0 produces clean React components for the upload dropzone, waveform visualizer, and transcript editor. Next.js API routes for Whisper proxy calls are well within V0's capability and avoid the Supabase Edge Function complexity.

1. Prompt V0 with the spec below to generate the transcription page, API route, and history components
2. In V0's Vars panel, add OPENAI_API_KEY as a server-side environment variable (no NEXT_PUBLIC_ prefix — this key must never reach the browser)
3. Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the Vars panel; run the SQL schema in Supabase SQL editor
4. Publish to production and test microphone recording on the HTTPS URL — V0's sandbox does not support audio capture

Starter prompt:

```
Build a Next.js transcription page at /transcribe with two tabs: 'Record' and 'Upload'. Record tab: MediaDevices.getUserMedia for microphone access; Web Audio API AnalyserNode drawing a waveform on a canvas; MediaRecorder with audio/webm; Stop button sends audio Blob as FormData to POST /api/transcribe; show processing spinner. Upload tab: react-dropzone accepting audio/* up to 25 MB; client-side file size check with error toast for files over 25 MB; upload to Supabase Storage at audio/{userId}/{filename}; send storage URL in JSON body to POST /api/transcribe. The /api/transcribe route: accepts either a multipart audio file or a JSON body with a storage_url; calls OpenAI Whisper API with model whisper-1, response_format verbose_json using OPENAI_API_KEY from process.env (never expose to client); returns transcript text and segments. Transcript display: editable textarea; Export as .txt button using Blob download. Save each result to a Supabase transcriptions table. History page at /transcribe/history lists past transcriptions. Handle permission-denied, file-too-large, and API errors with descriptive messages. Use Tailwind CSS and shadcn/ui components.
```

Limitations:

- V0 sometimes generates Web Speech API code (SpeechRecognition) which only works in Chrome/Edge and does not support iOS Safari — explicitly prompt 'use OpenAI Whisper API, not Web Speech API' if you see SpeechRecognition in the output
- V0 does not provision Supabase or create the database schema — run the SQL manually in the Supabase SQL editor
- next/image may be applied to waveform canvas elements incorrectly; if the canvas doesn't render, check for next/image wrapping and remove it

### Custom — fit 5/10, 1–2 weeks

Custom build unlocks Deepgram WebSocket streaming for true real-time transcription, multi-speaker diarization with named voice profiles, and HIPAA-compliant audio pipelines for medical or legal use cases.

1. Implement Deepgram Nova-2 via WebSocket: open a wss://api.deepgram.com/v1/listen connection with diarize: true and smart_format: true parameters; pipe MediaRecorder audio chunks directly into the socket; receive interim and final transcripts as JSON events
2. Build a speaker diarization display that color-codes transcript segments by speaker number and lets users rename 'Speaker 0' to 'John' for the full document
3. Implement audio chunking for recordings longer than 25 minutes: split at natural silence points using Web Audio API silence detection, transcribe each chunk, concatenate results adjusting word timestamps by chunk offset
4. Add a filler word removal post-processing step that strips 'um', 'uh', 'you know' from the final transcript text before saving

Limitations:

- Deepgram WebSocket streaming requires a persistent backend connection — not suitable for serverless Edge Functions; a small always-on server or Cloudflare Durable Object is needed
- HIPAA compliance requires Business Associate Agreements with both the transcription API provider and your hosting/storage provider — verify BAA availability before committing to a provider

## Gotchas

- **Microphone blocked in Lovable preview and V0 sandbox** — getUserMedia() throws NotAllowedError inside sandboxed iframes — which is exactly what Lovable's preview pane and V0's sandbox environment use. The microphone permission dialog never appears; the browser silently blocks the call. This leads builders to think their code is broken when the deployed version works fine. Fix: Test all microphone functionality exclusively on the published HTTPS URL. In Lovable, click Publish and open the published link on your phone or desktop browser. Add a UI banner in the Record tab during development: 'Microphone testing requires the published app — preview mode does not support camera or microphone access.' This sets the right expectation and prevents wasted debugging time.
- **Whisper API rejects files over 25 MB with a 413 error** — OpenAI Whisper has a hard 25 MB file size limit. A 1-hour Zoom recording at typical quality is 60–120 MB. When a large file is sent to the API, it returns HTTP 413 Request Entity Too Large — but AI-generated code often doesn't handle this error, so the user sees a generic 'something went wrong' toast with no explanation of what to do. Fix: Add client-side file size validation in react-dropzone's validator prop before the upload starts. Show a specific error: 'This file is 87 MB, which exceeds the 25 MB Whisper limit. Try compressing it first, or use a shorter recording.' For power users who regularly transcribe long recordings, add ffmpeg.wasm to split the audio into 3-minute chunks on the client, transcribe each chunk, and concatenate the results adjusting timestamps by the chunk offset.
- **CORS error when calling Whisper directly from the browser** — api.openai.com does not include the caller's origin in its CORS response headers when called from browser JavaScript. The browser blocks the request before it reaches OpenAI, and the browser console shows 'CORS policy: No Access-Control-Allow-Origin header'. Additionally, calling Whisper from the browser would require putting the API key in client-side code, where any user can read it from DevTools. Fix: Always proxy Whisper calls through a Supabase Edge Function or Next.js API route on the server. The Edge Function reads OPENAI_API_KEY from Deno.env.get() (stored in Supabase Secrets) and never exposes it to the client. This pattern also lets you add per-user rate limiting and usage logging in the proxy.
- **Web Speech API code generated instead of Whisper** — When prompting Lovable or V0 for 'speech to text', they frequently generate SpeechRecognition (Web Speech API) code. Web Speech API is Chrome and Edge only — it does not work in Safari, Firefox, or iOS WebKit. Users on iPhones open the app and see nothing happen when they tap Record, because SpeechRecognition is not available in their browser. Fix: Explicitly name OpenAI Whisper in your prompt: 'use OpenAI Whisper API via a Supabase Edge Function, not the browser SpeechRecognition API.' If you want live word display, use Web Speech API as a visual-only layer on supported browsers (firing on interim results) while still sending the final Blob to Whisper for the authoritative transcript — that gives real-time feel without sacrificing cross-browser compatibility.
- **Whisper verbose_json word timestamps drift in long recordings** — Whisper's verbose_json word-level timestamps are accurate for the first 5 minutes of audio. In longer recordings, timestamp drift accumulates — by minute 20, words can be off by 10–15 seconds. This makes click-to-seek features unreliable for meeting transcription of typical 45–60 minute calls. Fix: For recordings over 5 minutes, split audio into 3-minute chunks before sending to Whisper. Transcribe each chunk independently and offset the returned timestamps by the cumulative chunk start time before concatenating. Store chunks as an array in transcript_json so you can reconstruct the original recording for playback if needed.

## Best practices

- Always proxy Whisper API calls through a server-side function — never call api.openai.com from browser JavaScript, both for CORS reasons and to keep your API key secret
- Validate file size client-side before upload and show a specific, actionable error — 'exceeds 25 MB limit' is more helpful than a generic API error
- Use response_format: verbose_json in Whisper requests to get word-level timestamps from the start — retrofitting click-to-seek later requires a data model change if you only stored plain text
- Show a waveform visualizer during recording so users know the microphone is active — a blank recording UI looks identical to a broken one
- Set transcription status to 'processing' immediately on submit and poll or use Supabase Realtime to update when done — users who navigate away and come back should see their transcript ready
- Cache transcription results in the database by audio file hash — if two users upload the same Zoom recording, only call Whisper once
- Display accepted file formats explicitly in the dropzone ('MP3, M4A, WAV, WebM, OGG up to 25 MB') — this single UI copy change reduces support requests about why uploads fail

## Frequently asked questions

### What's the cheapest way to add speech-to-text to a web app?

Web Speech API costs nothing — it's built into Chrome, Edge, and desktop Safari. The catch: it's English-heavy, not available on iOS Safari, and gives no transcript history or file upload. For a production app that needs cross-browser support and multi-language, OpenAI Whisper at $0.006/minute is the most cost-effective paid option. At 10 minutes of audio per user per month, 100 users costs roughly $6/mo in API fees — significantly less than most SaaS subscription tiers.

### Can I transcribe audio in multiple languages with Whisper?

Yes. Whisper supports 99 languages and auto-detects the language by default. To specify a language explicitly — which improves accuracy and skips the detection step — add a language parameter to the API request with the ISO 639-1 code ('es' for Spanish, 'fr' for French, 'de' for German). For multi-language meeting recordings where speakers switch languages mid-conversation, let Whisper auto-detect; the model handles code-switching reasonably well.

### How do I handle audio files larger than 25 MB for transcription?

Whisper enforces a hard 25 MB limit per request. For longer files, use ffmpeg.wasm in the browser to split the audio into 3-minute chunks before sending to your Edge Function. Each chunk goes to Whisper independently. Concatenate the results by offsetting the timestamps from each chunk by the cumulative start time of previous chunks. Store the chunk array in your transcript_json field so you can reconstruct the original timeline if needed.

### What's the difference between Web Speech API and Whisper API?

Web Speech API runs in the browser, is free, and gives you words in real time as the user speaks — but only works in Chrome and Edge (not iOS Safari or Firefox), is trained primarily on English, and produces no permanent transcript you can retrieve later. Whisper runs on OpenAI's servers, costs $0.006/minute, works in 99 languages, accepts uploaded audio files, and returns a permanent transcript with optional word timestamps. Use Web Speech API as a real-time 'live preview' layer on supported browsers, and send the final audio Blob to Whisper for the authoritative, cross-browser-compatible result.

### Can I show a live transcript while the user is still speaking?

For Chrome/Edge users: yes, use Web Speech API's onresult event which fires on interim and final results as speech is recognized. Words appear in real time. For iOS Safari and Firefox users: no native real-time option exists — the best you can do is show a waveform visualizer during recording and display the full Whisper transcript when they stop. A common pattern is to use Web Speech API for the live preview on supported browsers and always send the final audio to Whisper for the saved transcript, so the history is always Whisper-quality regardless of browser.

### How do I add speaker labels to a transcription?

Whisper does not natively diarize speakers (it transcribes what was said, not who said it). For speaker labels, either use Deepgram Nova-2 with diarize: true in the request parameters — it returns speaker numbers with each word — or run pyannote-audio as a post-processing step on your server to add speaker segmentation to a Whisper transcript. Deepgram is the simpler option at $0.0043/minute; it adds speaker labels without changing your frontend at all, just the API response schema.

### Is Whisper accurate enough for meeting transcription?

For English in a quiet environment, Whisper achieves roughly 5% word error rate — good enough for meeting notes where humans will review and correct. Accuracy drops in meetings with crosstalk, strong accents, or heavy technical jargon. For critical workflows (legal depositions, medical dictation), pair Whisper with post-processing to flag low-confidence segments using the avg_logprob values in verbose_json, and always build in an editing step before the transcript is finalized.

### How do I export a transcript as a Word document?

Use the docx.js library (^8.x) to generate a .docx file client-side. Create a Document with a Paragraph for each transcript segment or speaker block, then call Packer.toBlob(doc) and trigger a download with URL.createObjectURL(). This runs entirely in the browser — no server required for export. For PDF export, jsPDF is the equivalent client-side library. Both add roughly 50 lines of code and no additional API cost.

---

Source: https://www.rapidevelopers.com/app-features/audio-to-text-conversion
© RapidDev — https://www.rapidevelopers.com/app-features/audio-to-text-conversion
