DEV Community

VoiceDeveloper
VoiceDeveloper

Posted on

Using ElevenLabs in Electron Desktop Apps

Why Voice AI Belongs in Your Electron App

Desktop apps have come a long way from static windows to rich, interactive experiences. Adding a natural‑sounding voice can make your tool feel more alive—whether you’re building a note‑taking assistant, an accessibility helper, or a game‑like UI. Text‑to‑speech (TTS) engines have traditionally been clunky, but modern services now deliver studio‑grade voice cloning with low latency and a simple HTTP API.

For Electron developers, the sweet spot is a service that offers:

  • High‑quality, human‑like output – users shouldn’t hear “robotic” speech.
  • Fast, streaming‑ready responses – UI shouldn’t freeze while the audio is generated.
  • Easy authentication and clear docs – you want to spend time coding, not fighting auth.

That’s why I recommend ElevenLabs. Their API produces natural voices, supports custom voice cloning, and works nicely with the fetch‑based workflow you already use in Electron. You can sign up through my affiliate link here: https://try.elevenlabs.io/kr07zfuqn1bp.

Below is a step‑by‑step guide that shows you how to wire ElevenLabs into an Electron desktop app, from API key handling to playing the resulting audio in the renderer process.


Prerequisites

Requirement Why it matters
Node ≥ 16 Required for native fetch and modern JavaScript syntax.
Electron ≥ 22 Gives you a built‑in contextBridge for safe IPC.
ElevenLabs API key You’ll need a key from the dashboard (sign up via the link above).
Basic knowledge of IPC We’ll pass the generated audio URL from the main process to the UI.

Create a fresh Electron project (or use an existing one):

npx create-electron-app my-voice-app
cd my-voice-app
npm install node-fetch@2   # fetch works in the main process, but we pin v2 for simplicity
Enter fullscreen mode Exit fullscreen mode

Setting Up the Main Process

The main process will be responsible for contacting ElevenLabs, converting the returned audio buffer into a temporary file, and sending the file path back to the renderer.

1. Store the API key securely

Never hard‑code the key. Use an environment variable or Electron’s secure storage. For this demo we’ll use a .env file (remember to add it to .gitignore).

npm install dotenv
Enter fullscreen mode Exit fullscreen mode

main.js

require('dotenv').config();
const { app, BrowserWindow, ipcMain } = require('electron');
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // see next section
    },
  });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);
Enter fullscreen mode Exit fullscreen mode

2. Create a helper to call ElevenLabs

ElevenLabs expects a POST to /v1/text-to-speech/{voice_id} with JSON payload. We’ll use the “default” voice (EXAVITQu4vr4xnSDxMaL) for simplicity.

async function synthesizeSpeech(text) {
  const apiKey = process.env.ELEVENLABS_API_KEY;
  if (!apiKey) throw new Error('ELEVENLABS_API_KEY missing in env');

  const voiceId = 'EXAVITQu4vr4xnSDxMaL'; // you can replace with a cloned voice ID
  const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'xi-api-key': apiKey,
      'Content-Type': 'application/json',
      'Accept': 'audio/mpeg',
    },
    body: JSON.stringify({
      text,
      voice_settings: {
        stability: 0.75,
        similarity_boost: 0.85,
      },
    }),
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`ElevenLabs error: ${response.status} ${err}`);
  }

  // Return raw audio bytes
  return Buffer.from(await response.arrayBuffer());
}
Enter fullscreen mode Exit fullscreen mode

3. Expose an IPC endpoint

The renderer will send a tts-request message with the text. The main process replies with the path to a temporary .mp3 file.

ipcMain.handle('tts-request', async (event, text) => {
  try {
    const audioBuffer = await synthesizeSpeech(text);
    const tempPath = path.join(app.getPath('temp'), `tts-${Date.now()}.mp3`);
    fs.writeFileSync(tempPath, audioBuffer);
    return { success: true, filePath: tempPath };
  } catch (err) {
    console.error(err);
    return { success: false, error: err.message };
  }
});
Enter fullscreen mode Exit fullscreen mode

That’s all the main‑process work. Next, we bridge this safely to the renderer.


Securely Bridging to the Renderer

Electron’s contextBridge lets you expose a minimal API without giving the UI full Node access.

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('voiceAPI', {
  speak: async (text) => {
    const result = await ipcRenderer.invoke('tts-request', text);
    return result;
  },
});
Enter fullscreen mode Exit fullscreen mode

Now the renderer can call window.voiceAPI.speak('Hello world') and receive the file path.


Building the UI

We’ll keep the UI minimal: an input box, a “Speak” button, and an HTML5 <audio> element.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ElevenLabs Voice Demo</title>
  <style>
    body { font-family: sans-serif; margin: 2rem; }
    #status { margin-top: 1rem; color: #555; }
  </style>
</head>
<body>
  <h1>ElevenLabs TTS in Electron</h1>
  <textarea id="txt" rows="4" cols="50" placeholder="Type something..."></textarea><br/>
  <button id="speakBtn">Speak</button>
  <div id="status"></div>
  <audio id="player" controls></audio>

  <script src="renderer.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

renderer.js

const txt = document.getElementById('txt');
const btn = document.getElementById('speakBtn');
const status = document.getElementById('status');
const player = document.getElementById('player');

btn.addEventListener('click', async () => {
  const text = txt.value.trim();
  if (!text) return;

  status.textContent = 'Generating voice…';
  btn.disabled = true;

  const result = await window.voiceAPI.speak(text);
  if (result.success) {
    player.src = `file://${result.filePath}`;
    player.play();
    status.textContent = 'Playing…';
  } else {
    status.textContent = `Error: ${result.error}`;
  }

  btn.disabled = false;
});
Enter fullscreen mode Exit fullscreen mode

That’s a functional voice‑enabled desktop app! When you type a sentence and press Speak, the renderer asks the main process to call ElevenLabs, writes the MP3 to a temporary location, and streams it back to the UI.


Handling Streaming (Optional)

If you need sub‑second latency, you can switch to ElevenLabs’ streaming endpoint (/v1/text-to-speech/{voice_id}/stream). The response is a chunked audio stream that you can pipe directly into a ReadableStream and feed to the Web Audio API. The basic idea is:

// Inside synthesizeSpeech, replace the fetch options:
const response = await fetch(`${url}/stream`, { /* same headers */ });
const reader = response.body.getReader();
let chunks = [];
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(value);
}
return Buffer.concat(chunks);
Enter fullscreen mode Exit fullscreen mode

You’d then feed the buffer to an AudioContext instead of writing a file. This is more advanced, but worth exploring for real‑time assistants.


Security & Best Practices

  1. Never expose the API key to the renderer. The key lives only in the main process (or a secure native module).
  2. Rate‑limit your requests. ElevenLabs imposes limits per account; add a debounce on the UI side if you expect rapid calls.
  3. Cache repeated phrases. If your app often repeats the same prompts, store the generated MP3 locally to avoid extra API calls.
  4. Respect voice cloning policies. If you create custom voices, ensure you have permission from the source speaker and comply with ElevenLabs’ terms.

Tips for Better Voice Output

Tip Why it matters
Adjust stability and similarity_boost Tweaking these numbers can make the voice sound more expressive or more consistent with a cloned voice.
Use SSML tags ElevenLabs supports a subset of Speech Synthesis Markup Language (e.g., <break time="500ms"/>). This helps you control pacing without chopping the text.
Pre‑process user input Strip out emojis, excessive whitespace, or unsupported characters to avoid API errors.
Leverage voice cloning Upload a short 30‑second sample of your own voice via the dashboard, then use the returned voice ID to give your app a unique personality.

Wrap‑Up

Integrating high‑quality TTS into an Electron app is now a matter of a few lines of JavaScript. By using ElevenLabs (sign up through https://try.elevenlabs.io/kr07zfuqn1bp), you get studio‑grade synthesis, low latency, and a straightforward REST API that fits naturally into Electron’s main/renderer architecture.

Give it a spin: create a quick prototype, experiment with voice cloning, and see how a natural voice can transform the user experience of your desktop tool.

Ready to add a human touch to your app? Grab your API key via the affiliate link above, follow the steps, and start building voice‑first experiences today! Happy coding!

Top comments (0)