LLM Guides

Give Your JavaScript App a Voice with Speech-to-Text

Aug 14, 2026

Typing is fine until the user is driving, cooking, walking, holding a phone with one hand, filling out a form on a tiny screen, or staring at a support box thinking, “I do not want to type this whole thing.”

That is where speech-to-text starts feeling less like a fancy AI feature and more like basic product mercy.

A user taps a microphone button. They say what they need. The app records the audio, sends it for transcription, and turns the result into clean text that can be searched, summarized, saved, routed, or used inside a workflow.

For a JavaScript app, there are a few ways to do this:

  • Use the browser’s built-in Web Speech API for quick in-browser recognition.
  • Use MediaRecorder to capture audio and send it to a backend.
  • Use a speech-to-text API for more reliable transcripts.
  • Use LLMAPI after transcription to clean, structure, summarize, or route the text.
  • Store the transcript in a format your app can actually use.

In this guide, we’ll build a practical JavaScript speech-to-text workflow that lets users talk instead of type, then turns audio into clean transcripts for chat apps, note tools, support forms, search boxes, CRMs, meeting tools, and voice-enabled workflows.

The voice feature we are building

A speech-to-text feature usually has four parts.

PartWhat it does
CaptureRecords microphone audio in the browser
UploadSends audio to your backend
TranscribeConverts speech into text
Clean and useFormats transcript for your app

A simple flow looks like this:

user taps microphone
→ browser records audio
→ audio blob goes to backend
→ backend sends audio to transcription API
→ transcript comes back
→ LLMAPI cleans or structures the text
→ app shows usable text

That can power a lot of features:

AppSpeech-to-text use case
Chat appVoice messages converted into text
Support toolSpoken complaint becomes ticket text
Notes appDictated notes become searchable notes
CRMSales rep records call notes
Healthcare admin toolStaff dictate non-diagnostic notes
Education appStudents answer by voice
Accessibility featureUsers speak instead of typing
Search appVoice query becomes searchable text
Meeting appAudio becomes transcript and action items

The user sees a microphone button.

The product needs a clean audio pipeline behind it.

Browser speech recognition or API transcription?

JavaScript gives us two main paths.

PathHow it worksBest for
Web Speech APIBrowser handles recognition directlyQuick demos, simple voice commands, lightweight dictation
Audio upload + APIBrowser records audio, backend sends it to STT providerProduction apps, stored transcripts, longer audio, cleaner workflow control

The Web Speech API includes speech recognition and speech synthesis capabilities. MDN describes SpeechRecognition as the controller interface for the browser’s recognition service, while the broader Web Speech API includes both speech recognition and text-to-speech.

The API transcription path usually gives us more control. We can store audio, retry failed jobs, choose providers, request timestamps, apply diarization if supported, and run post-processing with LLMAPI.

For a serious product, we usually want the second path.

For a quick voice search or prototype, browser recognition can be enough.

Why we can write this guide

We’ve spent around 6 years working with AI APIs, speech-to-text workflows, JavaScript backends, browser recording, LLM post-processing, structured outputs, and developer tutorials. We also checked current documentation from MDN, OpenAI audio transcription docs, Deepgram speech-to-text docs, and LLMAPI docs while preparing this article.

A few docs matter here. MDN’s MediaStream Recording API documentation explains that MediaRecorder records media from a MediaStream and gives the recorded data back for processing. MDN’s Web Speech API docs describe in-browser recognition and speech synthesis, which is useful for lightweight voice interfaces. OpenAI’s speech-to-text docs describe transcription endpoints that accept audio files and return transcript formats, while its API reference lists common audio formats such as mp3, mp4, mpeg, mpga, m4a, ogg, wav, and webm. LLMAPI’s quick-start docs show an OpenAI-compatible API gateway pattern for chat completions, which makes it useful for transcript cleanup, formatting, summaries, and downstream text workflows.

The practical lesson is simple: the browser can capture audio, a speech-to-text model can transcribe it, and LLMAPI can help turn that raw transcript into app-ready text.

What a good transcript should return

A basic transcript is just text.

{
  "text": "I need help because I was charged twice for my subscription."
}

That is useful, but many apps need more.

A better response might include:

{
  "text": "I need help because I was charged twice for my subscription.",
  "clean_text": "I need help because I was charged twice for my subscription.",
  "intent": "billing_support",
  "language": "en",
  "confidence": 0.94,
  "warnings": [],
  "created_at": "2026-08-24T15:18:00-05:00"
}

For longer audio, add segments:

{
  "text": "I tried uploading the file twice, but it failed both times.",
  "segments": [
    {
      "start": 0.0,
      "end": 2.4,
      "text": "I tried uploading the file twice,"
    },
    {
      "start": 2.4,
      "end": 4.8,
      "text": "but it failed both times."
    }
  ]
}

A transcript becomes more useful when the app knows what to do with it.

The architecture we’ll use

We’ll use this setup:

LayerResponsibility
FrontendAsk for mic permission, record audio, send file
BackendReceive audio, validate file, call transcription API
Transcription providerTurn speech into text
LLMAPIClean transcript, extract intent, create structured output
App databaseStore transcript and metadata
UIShow transcript, let user edit, continue workflow

We’ll use JavaScript on both sides:

  • Browser JavaScript for recording.
  • Node.js and Express for the backend.
  • LLMAPI for transcript cleanup and structured text output.

Step 1: Create the Node.js project

Create a new project:

mkdir javascript-speech-to-text
cd javascript-speech-to-text
npm init -y

Install packages:

npm install express multer dotenv openai cors

Add this to package.json so we can use import syntax:

{
  "type": "module"
}

Create .env:

PORT=3000
LLMAPI_API_KEY=your_llmapi_key_here
LLMAPI_BASE_URL=https://api.llmapi.ai/v1

Keep the API key on the backend.

A microphone feature that leaks API keys to the browser is asking for trouble in surround sound.

Step 2: Add a basic Express server

Create server.js:

import express from "express";
import cors from "cors";
import dotenv from "dotenv";

dotenv.config();

const app = express();

app.use(cors());
app.use(express.json());

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Speech-to-text server running on port ${port}`);
});

Run it:

node server.js

Check:

curl http://localhost:3000/health

Step 3: Record audio in the browser

The browser needs microphone permission.

We can use navigator.mediaDevices.getUserMedia() to request audio, then MediaRecorder to record it.

MDN explains that the MediaStream Recording API uses MediaRecorder to record media from a stream, while getUserMedia() can provide microphone input as a MediaStream.

Create index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>JavaScript Speech-to-Text Demo</title>
  </head>
  <body>
    <h1>Speech-to-Text Demo</h1>

    <button id="startBtn">Start recording</button>
    <button id="stopBtn" disabled>Stop recording</button>

    <p id="status">Ready.</p>

    <h2>Transcript</h2>
    <textarea id="transcript" rows="8" cols="80"></textarea>

    <script src="./app.js"></script>
  </body>
</html>

Create app.js:

let mediaRecorder;
let audioChunks = [];

const startBtn = document.querySelector("#startBtn");
const stopBtn = document.querySelector("#stopBtn");
const statusEl = document.querySelector("#status");
const transcriptEl = document.querySelector("#transcript");

startBtn.addEventListener("click", async () => {
  audioChunks = [];

  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true
  });

  mediaRecorder = new MediaRecorder(stream, {
    mimeType: "audio/webm"
  });

  mediaRecorder.addEventListener("dataavailable", event => {
    if (event.data.size > 0) {
      audioChunks.push(event.data);
    }
  });

  mediaRecorder.addEventListener("stop", async () => {
    const audioBlob = new Blob(audioChunks, {
      type: "audio/webm"
    });

    statusEl.textContent = "Uploading audio for transcription...";

    const transcript = await uploadAudio(audioBlob);

    transcriptEl.value = transcript.clean_text || transcript.text || "";
    statusEl.textContent = "Done.";

    stream.getTracks().forEach(track => track.stop());
  });

  mediaRecorder.start();

  startBtn.disabled = true;
  stopBtn.disabled = false;
  statusEl.textContent = "Recording...";
});

stopBtn.addEventListener("click", () => {
  mediaRecorder.stop();

  startBtn.disabled = false;
  stopBtn.disabled = true;
});

async function uploadAudio(audioBlob) {
  const formData = new FormData();

  formData.append("audio", audioBlob, "recording.webm");

  const response = await fetch("http://localhost:3000/transcribe", {
    method: "POST",
    body: formData
  });

  if (!response.ok) {
    throw new Error("Transcription request failed.");
  }

  return response.json();
}

Open index.html from a local dev server.

For example:

npx serve .

Browsers often require secure contexts for microphone access, so use localhost during development and HTTPS in production.

Step 4: Receive audio on the backend

Update server.js:

import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import multer from "multer";

dotenv.config();

const app = express();

app.use(cors());
app.use(express.json());

const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 25 * 1024 * 1024
  }
});

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

app.post("/transcribe", upload.single("audio"), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({
      error: "Audio file is required."
    });
  }

  return res.json({
    status: "received",
    filename: req.file.originalname,
    mime_type: req.file.mimetype,
    size_bytes: req.file.size
  });
});

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Speech-to-text server running on port ${port}`);
});

Now the frontend can send recorded audio to the backend.

Next, we need transcription.

Step 5: Add a transcription function

The exact transcription endpoint depends on your chosen speech-to-text provider and the audio models available through your LLMAPI setup. The app architecture stays the same:

  1. Receive audio.
  2. Send audio to a speech-to-text endpoint.
  3. Get transcript text.
  4. Send transcript to LLMAPI for cleanup and structure.
  5. Return app-ready JSON.

A provider-style transcription helper can look like this.

Create transcriptionProvider.js:

export async function transcribeAudioFile(file) {
  /*
    Replace this function with your speech-to-text provider call.

    Expected return shape:
    {
      text: "raw transcript text",
      language: "en",
      confidence: 0.94,
      segments: []
    }
  */

  throw new Error("Connect your speech-to-text provider here.");
}

If your provider supports an OpenAI-compatible audio transcription endpoint, the request usually sends multipart/form-data with the audio file and model name. OpenAI’s audio docs describe transcription endpoints that take an audio file and return a transcript, and the API reference lists supported file formats including webm, which is useful because browsers often record WebM audio.

The backend should normalize the result into one internal shape even if you switch providers later.

{
  "text": "I need help because my file upload keeps failing.",
  "language": "en",
  "confidence": null,
  "segments": []
}

That keeps the rest of your app stable.

Step 6: Clean the transcript with LLMAPI

Raw transcripts can be messy.

Common issues:

  • Missing punctuation
  • Repeated words
  • Filler words
  • Wrong casing
  • Weird line breaks
  • Misheard product names
  • Rambling user speech
  • Half-finished sentences

LLMAPI can clean the transcript while preserving meaning.

Create llmapiClient.js:

import OpenAI from "openai";

export const llmapi = new OpenAI({
  apiKey: process.env.LLMAPI_API_KEY,
  baseURL: process.env.LLMAPI_BASE_URL || "https://api.llmapi.ai/v1"
});

Create cleanTranscript.js:

import { llmapi } from "./llmapiClient.js";

export async function cleanTranscript(rawTranscript) {
  const response = await llmapi.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `
You clean speech-to-text transcripts for a JavaScript app.

Return only valid JSON:
{
  "clean_text": "string",
  "summary": "string",
  "warnings": ["string"]
}

Rules:
- Preserve the user's meaning.
- Fix punctuation and obvious formatting issues.
- Do not invent details.
- Do not remove important uncertainty.
- Add warnings if the transcript is unclear or incomplete.
        `
      },
      {
        role: "user",
        content: rawTranscript
      }
    ],
    temperature: 0
  });

  return JSON.parse(response.choices[0].message.content);
}

LLMAPI’s quick-start docs show an OpenAI-compatible chat completions pattern, which is why this client style works for text cleanup and post-processing.

Step 7: Connect transcription and cleanup

Update the /transcribe route:

import { transcribeAudioFile } from "./transcriptionProvider.js";
import { cleanTranscript } from "./cleanTranscript.js";

app.post("/transcribe", upload.single("audio"), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({
        error: "Audio file is required."
      });
    }

    const rawResult = await transcribeAudioFile(req.file);
    const cleaned = await cleanTranscript(rawResult.text);

    return res.json({
      text: rawResult.text,
      clean_text: cleaned.clean_text,
      summary: cleaned.summary,
      language: rawResult.language || null,
      confidence: rawResult.confidence || null,
      segments: rawResult.segments || [],
      warnings: cleaned.warnings || []
    });
  } catch (error) {
    console.error(error);

    return res.status(500).json({
      error: "Transcription failed."
    });
  }
});

Now the app receives both raw and cleaned transcript text.

That is important because raw transcript text is useful for debugging, while cleaned text is nicer for users.

Step 8: Add browser-only speech recognition for quick voice input

For quick voice commands or short dictation, the Web Speech API can work directly in the browser.

Browser support and behavior vary, so treat this as a lightweight option. MDN notes that the Web Speech API provides speech recognition and synthesis, and SpeechRecognition.start() starts the recognition service to listen for incoming audio.

Example:

const SpeechRecognition =
  window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
  console.log("SpeechRecognition is not supported in this browser.");
} else {
  const recognition = new SpeechRecognition();

  recognition.lang = "en-US";
  recognition.interimResults = true;
  recognition.continuous = false;

  recognition.addEventListener("result", event => {
    let transcript = "";

    for (const result of event.results) {
      transcript += result[0].transcript;
    }

    console.log(transcript);
  });

  recognition.addEventListener("end", () => {
    console.log("Recognition ended.");
  });

  recognition.start();
}

This is good for:

  • Search boxes
  • Voice commands
  • Short text fields
  • Accessibility helpers
  • Quick prototypes
  • Browser-only demos

For production transcription that needs stored results, consistent formatting, long audio, timestamps, or provider choice, recording audio and sending it to your backend is usually easier to control.

Step 9: Add transcript intent detection

Once we have clean text, we can classify it.

Example use cases:

TranscriptIntent
“I was charged twice”billing_support
“The upload keeps failing”technical_support
“Cancel my subscription”cancellation
“Add this to my notes”create_note
“Search for refund policy”search_query
“Schedule a follow-up”task_request

Create classifyTranscript.js:

import { llmapi } from "./llmapiClient.js";

export async function classifyTranscript(cleanText) {
  const response = await llmapi.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `
Classify the user's transcript.

Return only valid JSON:
{
  "intent": "billing_support | technical_support | cancellation | create_note | search_query | task_request | other",
  "urgency": "low | medium | high",
  "entities": {
    "product": null,
    "date": null,
    "amount": null
  }
}

Rules:
- Use null when an entity is not clearly stated.
- Do not invent missing details.
        `
      },
      {
        role: "user",
        content: cleanText
      }
    ],
    temperature: 0
  });

  return JSON.parse(response.choices[0].message.content);
}

Now a spoken message can become workflow data.

{
  "intent": "billing_support",
  "urgency": "medium",
  "entities": {
    "product": null,
    "date": null,
    "amount": null
  }
}

This is where voice input becomes more than a text box.

Step 10: Return an app-ready response

Update the route again:

import { classifyTranscript } from "./classifyTranscript.js";

app.post("/transcribe", upload.single("audio"), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({
        error: "Audio file is required."
      });
    }

    const rawResult = await transcribeAudioFile(req.file);
    const cleaned = await cleanTranscript(rawResult.text);
    const classification = await classifyTranscript(cleaned.clean_text);

    return res.json({
      raw_text: rawResult.text,
      clean_text: cleaned.clean_text,
      summary: cleaned.summary,
      language: rawResult.language || null,
      confidence: rawResult.confidence || null,
      segments: rawResult.segments || [],
      intent: classification.intent,
      urgency: classification.urgency,
      entities: classification.entities,
      warnings: cleaned.warnings || []
    });
  } catch (error) {
    console.error(error);

    return res.status(500).json({
      error: "Transcription failed."
    });
  }
});

Example final response:

{
  "raw_text": "i got charged twice for the pro plan and i need someone to help",
  "clean_text": "I got charged twice for the Pro plan, and I need someone to help.",
  "summary": "The user reports a duplicate Pro plan charge and wants help.",
  "language": "en",
  "confidence": 0.94,
  "segments": [],
  "intent": "billing_support",
  "urgency": "medium",
  "entities": {
    "product": "Pro plan",
    "date": null,
    "amount": null
  },
  "warnings": []
}

Now the JavaScript app can:

  • Fill a text box.
  • Create a support ticket.
  • Route the issue.
  • Search docs.
  • Save a note.
  • Trigger a workflow.
  • Ask follow-up questions.

That is the product value.

Step 11: Add user editing

Always let users edit transcripts.

Speech-to-text can mishear:

  • Names
  • Product terms
  • Accents
  • Numbers
  • Email addresses
  • Dates
  • Technical words
  • Noisy audio
  • Brand names
  • Acronyms

A good UI should show:

  1. Transcript preview.
  2. Edit button.
  3. Confirm button.
  4. Retry recording button.
  5. Warning when audio was unclear.

Example frontend behavior:

async function handleTranscriptResult(result) {
  transcriptEl.value = result.clean_text || result.raw_text || "";

  if (result.warnings?.length) {
    statusEl.textContent = result.warnings.join(" ");
  } else {
    statusEl.textContent = "Transcript ready. Please review before submitting.";
  }
}

Voice input should feel helpful, not bossy.

Users should stay in control of the final text.

Step 12: Add recording limits

Do not let users accidentally record a 90-minute monologue into a tiny form field.

Add limits:

LimitWhy
Max recording timeControls cost and latency
Max file sizeProtects backend
Allowed MIME typesAvoids unsupported uploads
Silence timeoutStops dead recordings
Retry limitAvoids abuse
User quotaControls usage
Workspace quotaProtects team plans

Frontend max duration:

let maxRecordingTimer;

startBtn.addEventListener("click", async () => {
  audioChunks = [];

  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true
  });

  mediaRecorder = new MediaRecorder(stream, {
    mimeType: "audio/webm"
  });

  mediaRecorder.start();

  maxRecordingTimer = setTimeout(() => {
    if (mediaRecorder.state === "recording") {
      mediaRecorder.stop();
      statusEl.textContent = "Recording stopped after the maximum duration.";
    }
  }, 60 * 1000);
});

stopBtn.addEventListener("click", () => {
  clearTimeout(maxRecordingTimer);
  mediaRecorder.stop();
});

Backend limits should still exist because frontend limits can be bypassed.

Step 13: Handle long audio differently

Short voice input can be handled immediately.

Long audio needs jobs.

Use async processing for:

  • Meetings
  • Interviews
  • Podcasts
  • Lectures
  • Calls
  • Voice notes over a few minutes
  • Uploaded audio files
  • Batch transcription

A long-audio API flow:

upload audio
→ create transcription job
→ return job_id
→ process in worker
→ save transcript
→ frontend polls job status

Job response:

{
  "job_id": "job_123",
  "status": "queued"
}

Status response:

{
  "job_id": "job_123",
  "status": "processing",
  "progress": 48
}

Finished response:

{
  "job_id": "job_123",
  "status": "completed",
  "transcript_url": "/transcripts/job_123"
}

Do this before users start uploading full podcast episodes into a synchronous route.

Step 14: Add timestamps when useful

Short dictation does not always need timestamps.

Longer audio often does.

Timestamps help with:

  • Captions
  • Meeting playback
  • Call review
  • Search results
  • Audio clipping
  • Podcast chapters
  • QA workflows
  • Evidence review
  • Jump-to-moment UI

Segment format:

{
  "segments": [
    {
      "start": 0.0,
      "end": 3.2,
      "text": "I need help with my subscription."
    },
    {
      "start": 3.2,
      "end": 7.1,
      "text": "I think I was charged twice."
    }
  ]
}

If your transcription provider supports timestamps, store them.

Even when you do not need them today, they are hard to recreate later.

Step 15: Add language handling

Voice apps often need language support.

At minimum, track:

  • User-selected language
  • Auto-detected language
  • Transcript language
  • Translation need
  • Unsupported language warnings

Frontend language selector:

<label for="language">Language</label>
<select id="language">
  <option value="en">English</option>
  <option value="es">Spanish</option>
  <option value="uk">Ukrainian</option>
  <option value="pl">Polish</option>
</select>

Send it with the audio:

const languageEl = document.querySelector("#language");

formData.append("language", languageEl.value);

Backend:

app.post("/transcribe", upload.single("audio"), async (req, res) => {
  const language = req.body.language || "en";

  const rawResult = await transcribeAudioFile(req.file, {
    language
  });

  const cleaned = await cleanTranscript(rawResult.text);

  res.json({
    language,
    raw_text: rawResult.text,
    clean_text: cleaned.clean_text
  });
});

Language metadata matters when you later add translation, multilingual search, or locale-specific formatting.

Step 16: Add a voice search feature

Speech-to-text works nicely for search.

Flow:

user speaks query
→ transcription
→ clean query
→ semantic or keyword search
→ results

Example transcript:

how do I get my money back if I was charged twice

LLMAPI can clean it into:

How do I get a refund if I was charged twice?

Then your search system can use that.

A voice search response:

{
  "clean_text": "How do I get a refund if I was charged twice?",
  "intent": "search_query",
  "search_results": [
    {
      "title": "Refund policy",
      "url": "/docs/refunds"
    },
    {
      "title": "Duplicate charges",
      "url": "/docs/duplicate-charges"
    }
  ]
}

This is a great use case because users often speak search queries naturally.

They do not need perfect grammar. They need the app to understand the request.

Step 17: Add support ticket creation

Another useful workflow:

voice complaint
→ transcript
→ clean text
→ classify intent
→ create ticket draft
→ user reviews and submits

LLMAPI can generate a ticket draft:

export async function createTicketDraft(cleanText) {
  const response = await llmapi.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `
Create a support ticket draft from the user's transcript.

Return only valid JSON:
{
  "title": "string",
  "description": "string",
  "category": "billing | technical | account | other",
  "priority": "low | medium | high",
  "missing_information": ["string"]
}

Rules:
- Use only the transcript.
- Do not invent account details.
- Keep the title short.
        `
      },
      {
        role: "user",
        content: cleanText
      }
    ],
    temperature: 0
  });

  return JSON.parse(response.choices[0].message.content);
}

Example:

{
  "title": "Duplicate Pro plan charge",
  "description": "The user says they were charged twice for the Pro plan and needs help resolving the billing issue.",
  "category": "billing",
  "priority": "medium",
  "missing_information": [
    "Invoice number",
    "Payment date",
    "Amount charged"
  ]
}

That is much more useful than dumping raw transcript text into a ticket field.

Step 18: Add transcript safety and privacy rules

Audio can contain sensitive information.

People say things they would never type carefully.

That can include:

  • Names
  • Emails
  • Phone numbers
  • Addresses
  • Account details
  • Payment details
  • Health information
  • Workplace issues
  • Legal details
  • Customer data
  • Private background speech

Basic rules:

  • Ask for microphone permission clearly.
  • Show when recording is active.
  • Let users stop recording easily.
  • Do not record without an explicit user action.
  • Keep API keys on the backend.
  • Validate file type and size.
  • Store audio only when needed.
  • Delete temporary audio after transcription if storage is unnecessary.
  • Avoid logging raw transcripts.
  • Add redaction for sensitive workflows.
  • Let users review before submitting.
  • Follow local consent and recording laws.

A clear UI message:

Tap the microphone to record your message. We use the recording to create a transcript, and you can review the text before submitting.

Do not hide recording behind vague UI.

A microphone feature should be obvious.

Step 19: Add redaction when needed

For some apps, redact sensitive data before storing or sending downstream.

Simple redaction example:

export function redactBasicSensitiveText(text) {
  return text
    .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[email]")
    .replace(/\+?\d[\d\s().-]{7,}\d/g, "[phone]");
}

Use it before logs:

const safeLogText = redactBasicSensitiveText(cleaned.clean_text);

console.log({
  event: "transcription_completed",
  transcript_preview: safeLogText.slice(0, 120)
});

For serious redaction, use a PII detection system or dedicated data protection workflow.

Regex is a start, not a compliance strategy.

Step 20: Handle errors without making the user feel cursed

Speech-to-text can fail for boring reasons:

ProblemBetter user message
Mic permission denied“Microphone access is blocked. Please allow microphone access and try again.”
No speech detected“We could not detect speech. Please try speaking closer to the microphone.”
Audio too long“This recording is too long. Please record a shorter message or upload a file.”
File too large“The audio file is too large for this feature.”
Provider timeout“Transcription is taking longer than expected. Please try again.”
Unsupported format“This audio format is not supported.”
Low confidence“The transcript may need review because the audio was unclear.”

Backend error shape:

{
  "error": {
    "code": "NO_SPEECH_DETECTED",
    "message": "We could not detect speech in this recording."
  }
}

Good voice UX needs calm failure states.

A raw API error in a voice feature feels especially bad because the user already did the awkward thing of talking to a website.

Step 21: Save transcripts with metadata

Store enough metadata for debugging and product workflows.

Example:

{
  "transcript_id": "tr_123",
  "user_id": "user_456",
  "source": "browser_microphone",
  "language": "en",
  "raw_text": "i got charged twice for the pro plan",
  "clean_text": "I got charged twice for the Pro plan.",
  "intent": "billing_support",
  "duration_seconds": 6.4,
  "provider": "speech_to_text_provider",
  "llm_model": "gpt-4o-mini",
  "created_at": "2026-08-24T15:18:00-05:00",
  "warnings": []
}

Useful metadata:

  • Transcript ID
  • User ID or workspace ID
  • Duration
  • Language
  • Audio source
  • Provider
  • Model
  • Raw text
  • Clean text
  • Intent
  • Warnings
  • User edits
  • Created date

User edits are especially useful.

If users keep correcting a product name, add custom vocabulary or better cleanup prompts.

Step 22: Use custom vocabulary where possible

Speech-to-text systems often struggle with:

  • Brand names
  • Product names
  • Internal tools
  • Acronyms
  • People’s names
  • Technical terms
  • Industry jargon

If your provider supports custom vocabulary, use it.

Examples:

{
  "custom_terms": [
    "LLMAPI",
    "Webhook",
    "RAG",
    "SAML",
    "Spendbase",
    "Parachute"
  ]
}

If provider-level vocabulary is unavailable, use LLMAPI cleanup after transcription.

For example:

If the transcript says "LM API" or "Ellem API" and the context is our product, normalize it to "LLMAPI".

Be careful with automatic corrections.

Only normalize terms when the context supports it.

Step 23: Add streaming later if needed

Some apps need live transcription.

Examples:

  • Live captions
  • Meeting transcription
  • Real-time assistants
  • Voice agents
  • Call center tools
  • Dictation with immediate feedback

Streaming is more complex because audio is sent in chunks while the user is still speaking.

A streaming flow:

microphone audio stream
→ websocket
→ transcription provider
→ partial transcript
→ final transcript segments
→ LLMAPI post-processing after final text

Deepgram’s materials describe real-time streaming speech-to-text, including endpointing behavior where the system detects pauses and finalizes results for a processed time range.

For most apps, start with recorded clips.

Add streaming when the product truly needs live behavior.

Recorded audio is much easier to debug.

Step 24: Common mistakes

MistakeBetter approach
Sending API keys from the browserKeep transcription calls on the backend
No transcript reviewLet users edit before submitting
Recording without clear UI stateShow active recording status
No file size limitAdd frontend and backend limits
Treating all audio as shortUse jobs for long recordings
Ignoring browser supportProvide fallback paths
Storing raw audio foreverUse retention rules
Logging raw transcriptsRedact or avoid sensitive logs
No language handlingLet users select or detect language
No custom vocabularyAdd product terms where possible
Using raw transcript directlyClean and structure it first
No error messagesExplain mic, audio, and provider issues clearly

The most common product mistake is treating voice input like a normal text field with a microphone icon attached.

Voice needs recording states, review, correction, privacy, and failure handling.

Where LLMAPI helps

LLMAPI is useful after speech becomes text.

Use it for:

NeedLLMAPI role
Transcript cleanupFix punctuation and formatting
SummaryTurn rambling speech into a short note
Intent detectionRoute spoken requests
Entity extractionPull dates, amounts, names, products
Ticket draftsCreate support-ready text
Search queriesClean spoken search
Meeting notesExtract decisions and action items
Translation supportPrepare transcript for multilingual workflows
Review warningsFlag unclear or incomplete transcripts
Product formattingConvert transcript into app-ready JSON

A strong voice workflow looks like this:

record audio
→ transcribe speech
→ clean transcript with LLMAPI
→ classify or structure text
→ let user review
→ save or trigger workflow

That makes the microphone button useful instead of decorative.

Closing notes for builders

Speech-to-text works best when the app respects how people actually speak.

People pause. They ramble. They restart sentences. They mispronounce product names. They say “uh” and “wait” and “actually forget that.” The transcript layer has to handle that mess without turning the user’s meaning into something too polished or wrong.

So build the feature in layers.

Use the browser to record audio. Send it to the backend. Transcribe with a speech-to-text provider. Use LLMAPI to clean, summarize, classify, or structure the result. Let users review the transcript before it becomes a ticket, note, search query, or workflow action.

When this is done well, the app feels easier to use.

The user speaks, the app listens, and the final text is clean enough to be useful.

Deploy in minutes