Feature spec
BeginnerCategory
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
Everything it takes to ship Speech Synthesis for Text — parts, prompts, and real costs.
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.
TTS Trigger Button
UIPlay 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
UIDropdown 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
UIRenders 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
UIThe 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
UIA 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)
BackendA 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
DataA 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):
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);910alter table public.tts_cache enable row level security;1112create policy "Service role can insert cache entries"13 on public.tts_cache for insert14 to service_role15 with check (true);1617create policy "Authenticated users can read cache"18 on public.tts_cache for select19 to authenticated20 using (true);2122create index tts_cache_hash_idx23 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.
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
- 1In FlutterFlow, add the flutter_tts package under Settings → Pubspec Dependencies and import it in a Custom Action
- 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
- 3Add a Custom Action 'PauseTTS' calling flutterTts.pause() and 'StopTTS' calling flutterTts.stop(); bind them to Pause and Stop buttons using the Action editor
- 4Wire setProgressHandler to update a Page State variable (currentWordIndex) and use a Conditional Widget to apply highlight styling to the current word span
- 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:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| ElevenLabs | Premium TTS with 1,000+ voices, natural prosody, and voice cloning | 10,000 characters/month | Starter $5/mo (~30K chars); Creator $22/mo (~100K chars) (approx) |
| Google Cloud Text-to-Speech | High-volume TTS with WaveNet and Standard voices; 30+ languages | 1M characters/month free (Standard voices) | $4–$16 per 1M characters depending on voice type (WaveNet vs Standard) (approx) |
| Web Speech API | Browser-native TTS; zero cost; uses OS voices; no API key required | Unlimited, browser-native | Free |
| flutter_tts | Open-source Flutter package wrapping iOS AVSpeechSynthesizer and Android TTS engine | Unlimited, OS voices | Free |
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
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
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
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.
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.
Need this feature production-ready?
RapidDev builds speech synthesis for text into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.