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

- Tool: App Features
- Last updated: July 2026

## 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.

## 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.

- **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.
- **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.
- **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.
- **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.
- **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().
- **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.
- **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.

## 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):

```sql
create table public.tts_cache (
  id uuid primary key default gen_random_uuid(),
  text_hash text not null unique,
  voice_id text not null,
  storage_path text not null,
  duration_seconds int,
  created_at timestamptz not null default now()
);

alter table public.tts_cache enable row level security;

create policy "Service role can insert cache entries"
  on public.tts_cache for insert
  to service_role
  with check (true);

create policy "Authenticated users can read cache"
  on public.tts_cache for select
  to authenticated
  using (true);

create index tts_cache_hash_idx
  on public.tts_cache (text_hash);
```

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 paths

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

Lovable's ElevenLabs Shared Connector and native Supabase integration make the premium-voice path possible without writing backend code. On-device Web Speech API needs no backend at all.

1. Create a new Lovable project and connect Lovable Cloud so Supabase auth and storage are provisioned automatically
2. In Lovable's Cloud tab → AI section, add ElevenLabs as a Shared Connector and paste your ElevenLabs API key into Secrets
3. Paste the prompt below and let Agent Mode build the TTS player, voice selector, word highlighter, and Edge Function
4. Publish the project and test audio on the published URL — the Lovable preview iframe blocks autoplay audio due to browser autoplay policies

Starter prompt:

```
Build a text-to-speech feature for article pages. Each article has a body text string stored in Supabase. Add a TTS player bar at the top of the article with: a Play/Pause toggle button, a Stop button, a speed slider (0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x), and a voice selector dropdown (populate from window.speechSynthesis.getVoices() on page load using onvoiceschanged callback — do not call getVoices() synchronously). Use the Web Speech API (SpeechSynthesisUtterance) as the primary engine. Wire onboundary events to highlight the current word in the article body by wrapping each word in a <span> and applying a yellow background CSS class to the span matching charIndex. If the article text is over 4,800 characters, split it at sentence boundaries (. ! ?) before passing to SpeechSynthesis to avoid the 5,000-char limit. Add an Edge Function that accepts {text, voice_id} and calls ElevenLabs /v1/text-to-speech/{voice_id} with the ELEVENLABS_API_KEY from Secrets; return audio/mpeg. Before calling ElevenLabs, check the tts_cache table for a matching SHA-256 hash of (text+voice_id); if found, return the Supabase Storage signed URL instead. Fallback to Web Speech API if the Edge Function returns an error. Handle empty text with a disabled Play button state. Persist voice selection to localStorage.
```

Limitations:

- Lovable preview iframe blocks audio autoplay — always test TTS on the published URL, never inside the preview pane
- Word-boundary highlighting works reliably on Chrome and Edge but is inconsistent on Safari due to partial onboundary event support
- ElevenLabs Shared Connector in Lovable routes through Lovable's infrastructure; for production, move to a dedicated Edge Function with your own key

### Flutterflow — fit 5/10, 2–3 hours

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.

1. In FlutterFlow, add the flutter_tts package under Settings → Pubspec Dependencies and import it in a Custom Action
2. Create 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. Add a Custom Action 'PauseTTS' calling flutterTts.pause() and 'StopTTS' calling flutterTts.stop(); bind them to Pause and Stop buttons using the Action editor
4. Wire setProgressHandler to update a Page State variable (currentWordIndex) and use a Conditional Widget to apply highlight styling to the current word span
5. In 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

Limitations:

- 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

### Custom — fit 3/10, 3–5 days

Worth it when the app requires branded voice cloning, multi-language SSML, or an offline-first synthesis pipeline. Highest effort for features most apps don't need.

1. Implement the ElevenLabs voice cloning pipeline: record founder or narrator voice samples, upload via ElevenLabs API, store the cloned voice_id per project in Supabase
2. Build server-side SSML injection: parse article headings and lists into SSML tags (break time='500ms', emphasis) before sending to Google Cloud TTS
3. Implement the audio caching pipeline with SHA-256 keying, Supabase Storage upload, signed URL return, and 90-day TTL cleanup job
4. Ship offline support using the Web Cache API (service worker caches audio blobs) or Flutter's path_provider to store mp3 files locally

Limitations:

- Voice cloning via ElevenLabs requires a Creator plan ($22/mo) or higher; cloned voice approval takes 24–48 hours
- SSML with Google Cloud TTS WaveNet voices costs $16 per 1M characters — caching is mandatory at any meaningful scale

## Gotchas

- **Audio plays silently in Lovable preview but works on published URL** — 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** — 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** — 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** — 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** — 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

- Cache generated audio by SHA-256(text + voice_id) in Supabase Storage — repeat plays of popular articles should never hit the paid API
- Split text at sentence boundaries (not character count) before passing to any TTS engine, so audio never cuts off mid-word
- Always route ElevenLabs calls through a Supabase Edge Function; never let the API key appear in frontend JavaScript
- Call setIosAudioCategory(playback) on flutter_tts before the first speak() to prevent silent failure on iOS
- Wrap getVoices() in a speechSynthesis.onvoiceschanged callback on web — synchronous calls on page load return an empty array in Chrome
- Give the Play button a minimum 44×44px touch target and include a visible ARIA label for screen reader compatibility
- Show a character count when using ElevenLabs so users or editors can see when content will require multiple API chunks

## 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.

---

Source: https://www.rapidevelopers.com/app-features/speech-synthesis-for-text
© RapidDev — https://www.rapidevelopers.com/app-features/speech-synthesis-for-text
