VoiceDeveloperWhy a Text‑to‑Speech Chrome Extension? If you spend a lot of time reading articles,...
If you spend a lot of time reading articles, documentation, or even chat threads, you’ve probably wished for a quick way to listen instead of scrolling. A Chrome extension that turns any selected text into natural‑sounding speech can boost productivity, reduce eye strain, and make the web more accessible. In this post we’ll build a minimalist TTS extension that uses ElevenLabs – a state‑of‑the‑art voice AI platform – to generate high‑quality audio on the fly.
Note: The API key you obtain from ElevenLabs is free for a generous amount of usage, and the affiliate link below helps us keep the guide up‑to‑date. No extra cost for you.
blob: URL.
All the heavy lifting is done in JavaScript; we only need a tiny Python helper (optional) to fetch the API key securely if you prefer not to embed it directly.
manifest.json.Tip: Keep the key out of version control. In production you’d store it on a server and proxy the request, but for a local demo embedding it is fine.
tts-extension/
│
├─ manifest.json
├─ background.js
├─ content.js
├─ popup.html
├─ popup.js
└─ icons/
└─ icon48.png
manifest.json
We’ll target Manifest V3, the current standard for Chrome extensions.
{
"manifest_version": 3,
"name": "Quick TTS",
"description": "Select text and listen instantly using ElevenLabs AI voices.",
"version": "1.0",
"permissions": ["activeTab", "scripting", "storage"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"48": "icons/icon48.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
content.js)
The content script watches for a custom event that the popup will dispatch. When triggered, it reads the current selection and forwards it to the background script.
// content.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "speakSelection") {
const selection = window.getSelection().toString().trim();
if (!selection) {
sendResponse({ error: "No text selected." });
return;
}
// Forward the text to the background worker
chrome.runtime.sendMessage(
{ action: "ttsRequest", text: selection },
(response) => sendResponse(response)
);
// Keep the message channel alive for async response
return true;
}
});
background.js)
Here we call the ElevenLabs API. The endpoint expects a POST with JSON payload and returns an audio/mpeg stream.
// background.js
const ELEVENLABS_API_KEY = "YOUR_ELEVENLABS_API_KEY"; // <-- replace
async function fetchAudio(text) {
const url = "https://api.elevenlabs.io/v1/text-to-speech/EXAMPLE_VOICE_ID";
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"xi-api-key": ELEVENLABS_API_KEY
},
body: JSON.stringify({
text,
model_id: "eleven_monolingual_v1",
voice_settings: { stability: 0.5, similarity_boost: 0.75 }
})
});
if (!response.ok) {
const err = await response.text();
throw new Error(`ElevenLabs error: ${err}`);
}
return await response.arrayBuffer();
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === "ttsRequest") {
fetchAudio(msg.text)
.then((buffer) => {
const blob = new Blob([buffer], { type: "audio/mpeg" });
const url = URL.createObjectURL(blob);
sendResponse({ audioUrl: url });
})
.catch((err) => sendResponse({ error: err.message }));
// Asynchronous response
return true;
}
});
Important: Replace
EXAMPLE_VOICE_IDwith the voice you like from ElevenLabs. You can list your available voices via their API or UI.
popup.html)
A tiny UI with a button and a spinner.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Quick TTS</title>
<style>
body { font-family: sans-serif; width: 200px; padding: 10px; }
#playBtn { width: 100%; padding: 8px; }
#spinner { display: none; margin-top: 8px; }
</style>
</head>
<body>
<button id="playBtn">🔊 Speak Selection</button>
<div id="spinner">Loading…</div>
<script src="popup.js"></script>
</body>
</html>
popup.js)
The script triggers the content script, receives the audio URL, and plays it.
// popup.js
const btn = document.getElementById("playBtn");
const spinner = document.getElementById("spinner");
let audio = null;
btn.addEventListener("click", async () => {
btn.disabled = true;
spinner.style.display = "block";
// Ask the active tab's content script to grab the selection
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, { action: "speakSelection" }, (response) => {
spinner.style.display = "none";
btn.disabled = false;
if (response?.error) {
alert(response.error);
return;
}
// Play the returned audio URL
if (audio) audio.pause();
audio = new Audio(response.audioUrl);
audio.play();
});
});
Embedding the API key in the extension is fine for personal use, but you might want a server‑side proxy for production. Below is a minimal Flask endpoint that forwards the request to ElevenLabs while keeping the key hidden.
# server.py
from flask import Flask, request, jsonify, send_file
import requests
import io
import os
app = Flask(__name__)
ELEVEN_API_KEY = os.getenv("ELEVEN_API_KEY") # set in your environment
@app.route("/tts", methods=["POST"])
def tts():
data = request.json
voice_id = data.get("voice_id", "EXAMPLE_VOICE_ID")
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
resp = requests.post(
url,
json=data,
headers={"xi-api-key": ELEVEN_API_KEY, "Content-Type": "application/json"},
stream=True,
)
resp.raise_for_status()
return send_file(
io.BytesIO(resp.content),
mimetype="audio/mpeg",
as_attachment=False,
download_name="speech.mp3",
)
if __name__ == "__main__":
app.run(port=5000)
You would then change background.js to call http://localhost:5000/tts instead of the ElevenLabs endpoint directly.
chrome://extensions/.
tts-extension folder.
If everything works, you should hear the selected text spoken in the voice you chose from ElevenLabs.
ElevenLabs exposes several parameters:
| Parameter | Range | Effect |
|---|---|---|
stability |
0‑1 | Controls how steady the voice sounds. Lower values add more “human‑like” variance. |
similarity_boost |
0‑1 | Makes the voice more like the reference speaker. Higher values improve likeness but can reduce naturalness. |
Feel free to experiment by updating the voice_settings object in background.js. The changes are reflected instantly—no need to rebuild the extension.
chrome.storage.local to avoid re‑requesting the same sentence.
model_id.
manifest.json for a one‑key trigger.All of these enhancements are straightforward additions once you have the core pipeline in place.
ElevenLabs makes it easy to generate lifelike speech with just a few lines of code. Grab your API key through this link https://try.elevenlabs.io/kr07zfuqn1bp, follow the steps above, and you’ll have a functional TTS extension in under ten minutes. Happy coding, and enjoy the sound of the web!