Skip to main content
RapidDev - Software Development Agency
App Featuresai-features15 min read

How to Add Speech Synthesis to Your App — Copy-Paste Prompts Included

Adding text-to-speech needs three pieces: a synthesis trigger (Web Speech API for free on-device, ElevenLabs for premium quality), a playback controller with play/pause/speed controls, and an audio cache to avoid re-billing identical content. With Lovable or FlutterFlow you can ship a working TTS feature in 2–4 hours for $0–5/month at low volume. Costs scale with usage: ElevenLabs Starter is $5/month for ~30K characters.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Beginner

Category

ai-features

Build with AI

2–4 hours with Lovable or FlutterFlow

Custom build

3–5 days custom dev

Running cost

$0–5/mo up to 100 users (free tier); $5–22/mo at 1K users with ElevenLabs

Works on

Mobile

Everything it takes to ship Speech Synthesis for Text — parts, prompts, and real costs.

TL;DR

Adding text-to-speech needs three pieces: a synthesis trigger (Web Speech API for free on-device, ElevenLabs for premium quality), a playback controller with play/pause/speed controls, and an audio cache to avoid re-billing identical content. With Lovable or FlutterFlow you can ship a working TTS feature in 2–4 hours for $0–5/month at low volume. Costs scale with usage: ElevenLabs Starter is $5/month for ~30K characters.

What Speech Synthesis for Text Actually Is

Speech synthesis turns written content into spoken audio — so users can listen to articles, product descriptions, or notifications hands-free while commuting, exercising, or resting their eyes. The browser and mobile OS provide free on-device synthesis (Web Speech API on web, flutter_tts on Flutter), which is sufficient for many apps. Premium services like ElevenLabs and Google Cloud TTS deliver dramatically better voice quality with natural inflection, 1,000+ voice options, and SSML support. The real product decisions are: which voice quality tier your audience expects, whether you need word-by-word highlighting for accessibility, and how to cache audio to avoid re-billing users who replay the same content.

What users consider table stakes in 2026

  • Play, pause, and stop controls visible on every text block — not hidden behind a settings screen
  • Adjustable playback speed from 0.5× to 2× with a visible slider or preset buttons
  • Voice selection offering at least a male and a female voice; preference persists across sessions
  • Word-by-word highlight tracking that follows the audio in real time for accessibility and comprehension
  • Offline fallback using on-device synthesis when ElevenLabs or Google TTS is unavailable
  • Accessible controls with ARIA labels on web (aria-label='Play article') or Flutter Semantics on mobile

Anatomy of the Feature

Seven components across UI, backend, and data layers. AI tools get the play button right on first pass; word highlighting and the audio cache are where first builds break.

Layers:UIDataBackend

TTS Trigger Button

UI

Play icon rendered per text card or article block. On web uses the browser SpeechSynthesis interface from the Web Speech API. On Flutter uses the flutter_tts package. Button state toggles between Play, Pause, and Stop based on synthesis events.

Note: Use a single SpeechSynthesisUtterance instance per text block; creating multiple utterances simultaneously causes audio overlap.

Voice Selector

UI

Dropdown populated from window.speechSynthesis.getVoices() on web or flutter_tts.getVoices() on Flutter. Displays voice name and language code. User selection is persisted to localStorage on web or Supabase user preferences for logged-in users.

Note: Chrome loads voices asynchronously — always wrap getVoices() in a speechSynthesis.onvoiceschanged callback or a 200ms setTimeout, otherwise the list is empty on page load.

Playback Controller

UI

Renders play/pause/stop buttons and an optional seek bar. Wired to SpeechSynthesisUtterance events (onstart, onend, onboundary, onpause, onresume) on web; mirrors flutter_tts callbacks (setStartHandler, setCompletionHandler, setProgressHandler) on Flutter.

Note: SpeechSynthesis does not expose a true seek position — the seek bar is a progress approximation based on charIndex from onboundary events.

Word Highlighter

UI

The onboundary event fires at each word boundary, providing charIndex and charLength. These values map to span elements wrapping each word in the text body. A CSS transition (background-color, 150ms) animates the highlight smoothly as speech advances.

Note: Split the article text into <span> elements by word before rendering — the highlighter depends on this structure being present in the DOM.

Rate and Pitch Controls

UI

A range slider between 0.5 and 2.0 writes to SpeechSynthesisUtterance.rate before each speak() call. Pitch control (0.5–2.0) optional. On flutter_tts, setSpeechRate() and setPitch() are called before speak().

Note: Rate and pitch changes take effect on the next utterance, not mid-speech. Restart the utterance from the current word to apply immediately.

TTS Edge Function (premium voices)

Backend

A Supabase Edge Function receives the text chunk and voice_id, calls the ElevenLabs API (/v1/text-to-speech/{voice_id}) or Google Cloud Text-to-Speech, and streams back the audio/mpeg response. The API key lives in Supabase Cloud tab → Secrets, never in frontend code.

Note: Always proxy ElevenLabs through an Edge Function. Any direct frontend call exposes your API key — ElevenLabs' own abuse scanner sees approximately 1,200 leaked keys per day.

Audio Cache Table

Data

A Supabase table storing hash(text + voice_id) mapped to a Supabase Storage path and duration. Before calling ElevenLabs, the Edge Function checks this table first. Cache hits serve the stored audio file directly, eliminating redundant API charges for re-played or commonly read content.

Note: SHA-256 of (text + voice_id) is the cache key. Truncate cached audio after 90 days for storage hygiene unless content is static.

The data model

One table caches generated audio to prevent billing the same content twice. Run this in the Supabase SQL editor (Dashboard → SQL Editor → New query):

schema.sql
1create table public.tts_cache (
2 id uuid primary key default gen_random_uuid(),
3 text_hash text not null unique,
4 voice_id text not null,
5 storage_path text not null,
6 duration_seconds int,
7 created_at timestamptz not null default now()
8);
9
10alter table public.tts_cache enable row level security;
11
12create policy "Service role can insert cache entries"
13 on public.tts_cache for insert
14 to service_role
15 with check (true);
16
17create policy "Authenticated users can read cache"
18 on public.tts_cache for select
19 to authenticated
20 using (true);
21
22create index tts_cache_hash_idx
23 on public.tts_cache (text_hash);

Heads up: The Edge Function uses the service_role key for INSERT (cache write) and the anon key returns the storage_path to the client for playback. The storage bucket for audio files should be private with signed URL access.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Native iOS + AndroidFit for this feature:

flutter_tts provides full native iOS and Android TTS with zero API cost. FlutterFlow's Action editor wires speak(), pause(), and stop() directly. Best path for a mobile-only app.

Step by step

  1. 1In FlutterFlow, add the flutter_tts package under Settings → Pubspec Dependencies and import it in a Custom Action
  2. 2Create a Custom Action 'SpeakText' that calls flutterTts.setLanguage(), setSpeechRate(), setIosAudioCategory(IosTextToSpeechAudioCategory.playback), and speak(text); expose it in the Action editor on your Play button
  3. 3Add a Custom Action 'PauseTTS' calling flutterTts.pause() and 'StopTTS' calling flutterTts.stop(); bind them to Pause and Stop buttons using the Action editor
  4. 4Wire setProgressHandler to update a Page State variable (currentWordIndex) and use a Conditional Widget to apply highlight styling to the current word span
  5. 5In Settings → Permissions, add NSMicrophoneUsageDescription is not needed but do add a background audio mode in Info.plist via the Custom Actions if you need audio to continue when the screen locks

Where this path bites

  • Word-boundary highlighting requires a Custom Dart widget — FlutterFlow's visual builder cannot wire SpeechSynthesis boundary events without code
  • flutter_tts voice quality depends on the OS voices installed; users on older Android devices may have limited voice options
  • ElevenLabs integration from FlutterFlow requires routing through a Supabase Edge Function — FlutterFlow cannot call ElevenLabs directly with auth headers safely

Third-party services you'll need

On-device synthesis is free. Premium voice quality from cloud TTS services adds marginal cost that caching keeps low:

ServiceWhat it doesFree tierPaid from
ElevenLabsPremium TTS with 1,000+ voices, natural prosody, and voice cloning10,000 characters/monthStarter $5/mo (~30K chars); Creator $22/mo (~100K chars) (approx)
Google Cloud Text-to-SpeechHigh-volume TTS with WaveNet and Standard voices; 30+ languages1M characters/month free (Standard voices)$4–$16 per 1M characters depending on voice type (WaveNet vs Standard) (approx)
Web Speech APIBrowser-native TTS; zero cost; uses OS voices; no API key requiredUnlimited, browser-nativeFree
flutter_ttsOpen-source Flutter package wrapping iOS AVSpeechSynthesizer and Android TTS engineUnlimited, OS voicesFree

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Web Speech API or flutter_tts covers all synthesis at zero cost. ElevenLabs free tier (10K chars/mo) handles typical usage at this scale. Supabase free tier for the cache table.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Audio plays silently in Lovable preview but works on published URL

Symptom: Lovable's preview runs inside a sandboxed iframe. Browsers apply strict autoplay policies to iframes — audio triggered without a direct user gesture in the top-level document is blocked silently. The play button click inside the iframe doesn't count as a trusted gesture in all browsers.

Fix: Always test audio features on the published Lovable URL, not the preview pane. Use the QR code in the top-right to open the published URL on your phone.

No voices returned from window.speechSynthesis.getVoices() on page load

Symptom: Chrome and Chromium-based browsers load the voice list asynchronously after the page renders. Calling getVoices() synchronously on mount returns an empty array. This breaks the voice selector dropdown and can default the utterance to a generic robotic voice.

Fix: Wrap getVoices() in a speechSynthesis.onvoiceschanged callback. If the voices are already loaded when you register the handler, call getVoices() immediately; otherwise wait for the event. A 200ms setTimeout fallback also works for simpler implementations.

flutter_tts playback crashes on iOS but works on Android

Symptom: iOS requires the AVAudioSession category to be set to .playback before AVSpeechSynthesizer can output audio. If the audio session is in .ambient (default) or another category, speak() runs without error but produces no sound, which looks like a crash to the user.

Fix: Call flutterTts.setIosAudioCategory(IosTextToSpeechAudioCategory.playback) before the first speak() call. Do this in your Custom Action's init block, not on every speak call.

ElevenLabs API key exposed in frontend code

Symptom: When prompting Lovable or FlutterFlow for ElevenLabs integration, AI tools frequently generate a direct fetch() call to the ElevenLabs API with the key hardcoded in JavaScript. This exposes the key in browser DevTools and your Git repository, and ElevenLabs will revoke it.

Fix: Move all ElevenLabs calls to a Supabase Edge Function. Store the key in Cloud tab → Secrets as ELEVENLABS_API_KEY. The Edge Function receives {text, voice_id} from the frontend and returns the audio stream — the key never touches the client.

Long articles cut off mid-sentence

Symptom: ElevenLabs' free and Starter tiers enforce a 2,500-character request limit per API call. The Web Speech API silently drops text beyond approximately 32,767 characters in some browsers. Submitting full articles without chunking results in audio that stops abruptly mid-sentence.

Fix: Split text into sentence-boundary chunks (at . ! ? followed by a space) before passing to the TTS engine. Track the current chunk index to maintain correct word-highlight continuity as chunks play in sequence.

Best practices

1

Cache generated audio by SHA-256(text + voice_id) in Supabase Storage — repeat plays of popular articles should never hit the paid API

2

Split text at sentence boundaries (not character count) before passing to any TTS engine, so audio never cuts off mid-word

3

Always route ElevenLabs calls through a Supabase Edge Function; never let the API key appear in frontend JavaScript

4

Call setIosAudioCategory(playback) on flutter_tts before the first speak() to prevent silent failure on iOS

5

Wrap getVoices() in a speechSynthesis.onvoiceschanged callback on web — synchronous calls on page load return an empty array in Chrome

6

Give the Play button a minimum 44×44px touch target and include a visible ARIA label for screen reader compatibility

7

Show a character count when using ElevenLabs so users or editors can see when content will require multiple API chunks

When You Need Custom Development

AI tools handle standard play/pause TTS with voice selection comfortably. These requirements push past what a prompted build can deliver reliably:

  • App requires a branded voice persona — a cloned voice of a founder, narrator, or product character using ElevenLabs voice cloning
  • Multi-language TTS across 10+ locales with correct phoneme handling for non-Latin scripts (Arabic, Hindi, Japanese)
  • SSML required for dramatic pauses, reading speed variation per paragraph type, emphasis on product names, or pronunciation overrides for technical terms
  • Offline-first architecture where all synthesis must happen on-device with no API dependency for users in low-connectivity environments

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Does Web Speech API work on all mobile browsers?

Chrome on Android supports it well. Safari on iOS supports SpeechSynthesis but has inconsistent onboundary event firing, which breaks word-by-word highlighting. Firefox on Android does not support Web Speech API as of 2026. For guaranteed cross-browser support at scale, route through an ElevenLabs or Google Cloud TTS Edge Function.

Can users download the audio file after synthesis?

Yes if you use a cloud TTS service (ElevenLabs, Google TTS) — the Edge Function returns an audio/mpeg file you can serve with a signed Supabase Storage URL. The Web Speech API synthesizes audio in real time and doesn't expose a downloadable buffer. If downloads are important, cloud TTS and caching is the right path.

How do I keep ElevenLabs costs low with many users?

Cache every generated audio file by SHA-256(text + voice_id) in Supabase Storage. When the same article is replayed (which is the common case for popular content), serve the cached file without touching the ElevenLabs API. A cache hit rate of 70–80% is realistic for a content app, reducing your effective character cost proportionally.

What is the difference between on-device TTS and ElevenLabs quality?

On-device voices (Web Speech API, flutter_tts) use the OS speech engine — functional but robotic, with flat prosody and limited voice variety. ElevenLabs uses deep learning models trained on professional voice actors and produces natural-sounding speech with inflection, breathing, and emotional range. Most users notice the difference immediately. For a content-listening feature where audio is the primary experience, the quality gap is significant.

Does text-to-speech work offline?

On-device synthesis (Web Speech API on web, flutter_tts on Flutter) works entirely offline because it uses the operating system's built-in TTS engine. ElevenLabs and Google Cloud TTS require an internet connection. You can support offline by falling back to the OS engine when the network is unavailable, or by pre-caching commonly played audio files in Supabase Storage and storing them locally.

How do I highlight words as they are spoken?

On web, listen to the onboundary event on SpeechSynthesisUtterance. Each event fires with charIndex (position in text) and charLength. Pre-wrap each word in the article body in a <span> element. In the onboundary handler, remove the highlight class from the previous span and add it to the span containing the current charIndex. On Flutter, flutter_tts provides a setProgressHandler callback with start and end character positions for the same effect.

Can I add multiple language voices?

Yes. On web, window.speechSynthesis.getVoices() returns voices tagged with BCP 47 language codes (en-US, fr-FR, es-ES). Set SpeechSynthesisUtterance.lang to match the content language. On flutter_tts, call setLanguage('fr-FR') before speak(). For ElevenLabs, select a voice trained on the target language — ElevenLabs voices are language-specific, not universal.

Is there a character limit per TTS request?

ElevenLabs free and Starter tiers limit requests to 2,500 characters per call. The Web Speech API limit varies by browser implementation — Chrome enforces roughly 32,767 characters. Google Cloud TTS accepts up to 5,000 characters per request (15,000 with SSML). Always chunk long texts at sentence boundaries before sending, and track chunk index for accurate word highlighting.

RapidDev

Need this feature production-ready?

RapidDev builds speech synthesis for text into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.