跳到正文
DEV Community· VoiceDeveloper·· 3 小時前AI 評分30

在 Electron 桌面應用程式中使用 ElevenLabs

Using ElevenLabs in Electron Desktop Apps

AI 導讀

指南示範在 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

  1. Sign up at https://try.elevenlabs.io/kr07zfuqn1bp
  2. Navigate to API Keys in the dashboard.
  3. 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:

  1. Security – the API key never touches the renderer.
  2. 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 using node-fetch gives 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