在 Electron 桌面應用程式中使用 ElevenLabs
Using ElevenLabs in Electron Desktop Apps
指南示範在 Electron 桌面應用程式中整合 ElevenLabs REST API,將文字合成語音並在應用程式播放。實作把 API 呼叫及金鑰保留在主程序,透過 contextBridge 和 IPC 讓 renderer 發出合成請求,並將音訊寫入暫存檔。指南列出 Node ≥ 14、Electron ≥ 22 的要求,並示範以 curl 測試 API。
Why ElevenLabs is a Game‑Changer for Desktop Voice Apps
If you’ve ever tried to add text‑to‑speech (TTS) to an Electron app, you know the pain points: low‑quality voices, clunky latency, and a steep learning curve for API authentication. ElevenLabs cuts through that noise with ultra‑realistic, low‑latency speech synthesis and a simple REST API that fits right into the Node.js ecosystem. The result? Your desktop app can talk back to users with a natural voice in just a few lines of code.
Pro tip: The affiliate link below gives you a free trial and a little boost for the ElevenLabs team, so you can start experimenting right away: https://try.elevenlabs.io/kr07zfuqn1bp
What You’ll Need
| Requirement | Reason |
|---|---|
| Node ≥ 14 | Electron ships with a modern Node runtime. |
| Electron ≥ 22 | Supports context‑bridge and fetch in the main process. |
| ElevenLabs API key | You’ll get this from the ElevenLabs dashboard after signing up. |
| Optional: curl | Handy for quick endpoint testing. |
If you already have an Electron project, great—just follow the steps below. If you’re starting from scratch:
npx create-electron-app my-voice-app
cd my-voice-app
npm install
Getting an ElevenLabs API Key
- Sign up at https://try.elevenlabs.io/kr07zfuqn1bp
- Navigate to API Keys in the dashboard.
- Copy the generated key – you’ll need it in both the main and renderer processes.
Security tip: Store the key in an environment variable (
ELEVANLABS_API_KEY) and never commit it to source control.
Quick Curl Test (Optional)
Before diving into code, verify the API works with a simple curl command:
curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/your_voice_id" \
-H "xi-api-key: $ELEVANLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello from ElevenLabs!",
"model_id": "eleven_monolingual_v1",
"voice_settings": { "stability": 0.75, "similarity_boost": 0.85 }
}' \
--output hello.wav
If hello.wav plays back clearly, you’re ready to integrate.
Architecture Overview
In an Electron app you have two worlds:
| Process | Role |
|---|---|
| Main | Handles OS‑level tasks, makes HTTP calls, writes files. |
| Renderer | UI layer (React, Vue, etc.) – requests audio and plays it. |
We’ll keep the ElevenLabs calls in the main process for two reasons:
- Security – the API key never touches the renderer.
- Performance – the main process can stream the audio directly to a file or buffer.
We’ll expose a tiny IPC bridge (window.elevenLabs) that the renderer can call.
Step 1 – Install Dependencies
npm install node-fetch@2 # fetch API for Node 14‑16
npm install fs-extra # convenient file utilities
Note: Electron bundles a recent version of
fetch, but usingnode-fetchgives us consistent behavior across Node versions.
Step 2 – Create a Service Wrapper (main process)
Create src/elevenLabsService.js:
// src/elevenLabsService.js
const fetch = require('node-fetch');
const fs = require('fs-extra');
const path = require('path');
const ELEVEN_API = 'https://api.elevenlabs.io/v1';
const API_KEY = process.env.ELEVANLABS_API_KEY; // set in .env or launch script
if (!API_KEY) {
throw new Error('ElevenLabs API key not set! Use ELEVANLABS_API_KEY env var.');
}
/**
* Synthesize speech and write to a temporary file.
* @param {string} text - Text to speak.
* @param {string} voiceId - ElevenLabs voice ID.
* @returns {Promise<string>} - Path to the generated .wav file.
*/
async function synthesize(text, voiceId = 'EXAVITQu4vr4xnSDxMaL') {
const url = `${ELEVEN_API}/text-to-speech/${voiceId}`;
const body = {
text,
model_id: 'eleven_monolingual_v1',
voice_settings: { stability: 0.75, similarity_boost: 0.85 }
};
const response = await fetch(url, {
method: 'POST',
headers: {
'xi-api-key': API_KEY,
'Content-Type': 'application/json',
Accept: 'audio/mpeg' // can also request 'audio/wav'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const err = await response.text();
throw new Error(`ElevenLabs error: ${response.status} ${err}`);
}
// Stream the binary audio directly to a file
const outPath = path.join(
require('os').tmpdir(),
`eleven-${Date.now()}.mp3`
);
const fileStream = fs.createWriteStream(outPath);
await new Promise((resolve, reject) => {
response.body.pipe(fileStream);
response.body.on('error', reject);
fileStream.on('finish', resolve);
});
return outPath;
}
module.exports = { synthesize };
Step 3 – Wire Up IPC with Context Bridge
Edit src/main.js (or wherever you create the BrowserWindow):
// src/main.js
const { app, BrowserWindow, ipcMain, contextBridge } = require('electron');
const path = require('path');
const { synthesize } = require('./elevenLabsService');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // see next step
contextIsolation: true,
nodeIntegration: false
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
// IPC handler
ipcMain.handle('elevenlabs:speak', async (event, { text, voiceId }) => {
try {
const audioPath = await synthesize(text, voiceId);
return { success: true, path: audioPath };
} catch (err) {
console.error(err);
return { success: false, error: err.message };
}
});
Create src/preload.js to expose a safe API:
// src/preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('elevenLabs', {
/**
* Request speech synthesis.
* @param {string} text
* @param {string} [voiceId]
* @returns {Promise<{success:boolean, path?:string, error?:string}>}
*/
speak: (text, voiceId) => ipcRenderer.invoke('elevenlabs:speak', { text, voiceId })
});
Now the renderer can call window.elevenLabs.speak(...) without ever seeing the API key.
Step 4 – Play the Audio in the Renderer
Assuming you’re using plain HTML/JS for the demo:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ElevenLabs Voice Demo</title>
</head>
<body>
<h1>ElevenLabs in Electron</h1>
<textarea id="txt" rows="4" cols="50" placeholder="Enter text..."></textarea><br>
<button id="speakBtn">Speak</button>
<audio id="player" controls></audio>
<script>
const txt = document.getElementById('txt');
const btn = document.getElementById('speakBtn');
const player = document.getElementById('player');
btn.addEventListener('click', async () => {
const text = txt.value.trim();
if (!text) return;
btn.disabled = true;
btn.textContent = 'Synthesizing...';
const result = await window.elevenLabs.speak(text);
if (result.success) {
// Convert file:// path to a URL the <audio> tag can use
player.src = `file://${result.path}`;
player.play();
} else {
alert('Error: ' + result.error);
}
btn.disabled = false;
btn.textContent = 'Speak';
});
</script>
</body>
</html>
Why Not Stream Directly to the <audio> Element?
Electron’s security model isolates the renderer from the filesystem. By writing to a temporary file and then feeding the file:// URL to the <audio> tag, we avoid extra dependencies like WebAudio streaming buffers. For real‑time streaming, you could pipe the response into a MediaSource object, but the file‑based approach is simpler and works well for most desktop use‑cases.
Step 5 – Optional – Voice Cloning
ElevenLabs also offers voice cloning. After you upload a short sample (≈30 seconds), you’ll receive a new voice_id. Use that ID in the speak call:
await window.elevenLabs.speak('Hello, this is my cloned voice!', 'my_custom_voice_id');
The cloning workflow lives entirely on ElevenLabs’ dashboard, so your Electron code stays unchanged—just swap the ID.
Tips & Gotchas
| Issue | Fix |
|---|---|
| CORS errors in renderer | Keep all network calls in the main process. The renderer only talks via IPC. |
| Audio lag | Use the model_id that matches your latency needs (eleven_monolingual_v1 is fast). |
| Too many requests | ElevenLabs enforces rate limits; batch longer texts or debounce UI clicks. |
| File cleanup | Temporary files accumulate. Add a cleanup routine with fs.unlink after playback finishes. |
| Cross‑platform path handling | Use path.join(os.tmpdir(), …) as shown; works on Windows, macOS, Linux. |
Wrapping Up
Integrating ElevenLabs into an Electron desktop app is surprisingly straightforward once you separate concerns: the main process talks to the ElevenLabs API, the renderer stays sandboxed, and you get high‑fidelity speech with just a few lines of JavaScript. Whether you’re building a voice‑enabled note‑taker, an accessibility helper, or a chatbot that actually talks, this stack scales from prototypes to production.
Ready to give your app a voice that sounds human? Grab a free trial and start experimenting with the same API you just wired up: https://try.elevenlabs.io/kr07zfuqn1bp
Happy coding, and may your apps speak as clearly as you think!
來源:DEV Community · dev.to