# Build a Voice Cloning Demo App

> Source: <https://dev.to/voice_developer/build-a-voice-cloning-demo-app-bfo>
> Published: 2026-10-06 20:57:05+00:00

Voice AI is no longer a niche research topic; it’s a mainstream tool that powers chatbots, accessibility features, and even virtual assistants. If you’ve ever wanted to add a personal touch to an app—think a custom greeting or a character that speaks exactly like your favorite actor—you’ve probably wondered how to make that happen. Enter voice cloning: the ability to synthesize speech that sounds like a specific person from just a few minutes of audio.

Building a voice‑cloning demo app is surprisingly approachable today. With cloud‑based APIs that handle the heavy lifting, you can focus on the UX and the business logic instead of training deep learning models. In this guide, we’ll walk through a practical, developer‑friendly way to create a simple web app that records a user’s voice, clones it with ElevenLabs, and plays back synthesized speech. By the end, you’ll have a reusable template that you can adapt for anything from personalized e‑learning to immersive gaming.

| Item | Why it matters | How to get it | 
|---|---|---|
| **Python 3.10+** | Needed for the backend example. | `brew install python` /`apt install python3` | 
| **Node.js 18+** | Optional, if you want a JavaScript frontend. | `brew install node` | 
| **Git** | Version control. | `brew install git` | 
| **An ElevenLabs account** | Provides the voice‑cloning API key. | Sign up at [https://try.elevenlabs.io/kr07zfuqn1bp](https://try.elevenlabs.io/kr07zfuqn1bp) | 

**Tip**: The ElevenLabs link above is your entry point. It offers a free trial tier and a generous quota that’s perfect for prototyping.

The heavy lifting (model training, inference) happens in the cloud. Your app merely orchestrates requests and handles the responses.

```
# Store your key in an environment variable for safety
export ELEVENLABS_API_KEY="YOUR_API_KEY"
```

**Security Note**: Never hard‑code your key in public repos. Use environment variables or secret managers.

We’ll use FastAPI for its async support and simplicity. Install the dependencies:

```
pip install fastapi uvicorn aiohttp python-multipart
```

Create `main.py`:

``` python
import os
import uuid
import aiohttp
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
HEADERS = {
    "accept": "application/json",
    "xi-api-key": ELEVENLABS_API_KEY,
    "Content-Type": "application/json"
}

ELEVENLABS_BASE = "https://api.elevenlabs.io/v1"

@app.post("/clone")
async def clone_voice(file: UploadFile = File(...)):
    # Validate file type
    if file.content_type not in ("audio/wav", "audio/mp3"):
        raise HTTPException(status_code=400, detail="Unsupported file type")

    # Save to temp file
    tmp_path = f"/tmp/{uuid.uuid4()}.wav"
    with open(tmp_path, "wb") as f:
        f.write(await file.read())

    # Step 1: Create a new voice
    async with aiohttp.ClientSession() as session:
        async with session.post(
            f"{ELEVENLABS_BASE}/voices",
            json={
                "name": "Demo Clone",
                "samples": [tmp_path]
            },
            headers=HEADERS
        ) as resp:
            if resp.status != 200:
                raise HTTPException(status_code=500, detail="Voice creation failed")
            voice_resp = await resp.json()
            voice_id = voice_resp["voice_id"]

    # Return the voice ID
    return JSONResponse(content={"voice_id": voice_id})

@app.post("/synthesize")
async def synthesize(voice_id: str, text: str):
    async with aiohttp.ClientSession() as session:
        async with session.post(
            f"{ELEVENLABS_BASE}/voices/{voice_id}/synthesize",
            json={"text": text},
            headers=HEADERS
        ) as resp:
            if resp.status != 200:
                raise HTTPException(status_code=500, detail="Synthesis failed")
            audio_url = (await resp.json())["audio_url"]

    # Stream the audio back to the client
    async with session.get(audio_url) as audio_resp:
        audio_bytes = await audio_resp.read()

    return JSONResponse(content={"audio": audio_bytes.hex()})
```

`/clone`` voice_id`.`/synthesize`
**Remember**: The ElevenLabs link used here is the same for all API calls, ensuring consistent authentication.

Below is a minimal HTML/JS snippet that records audio, calls the clone endpoint, then synthesizes text.

```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Voice Clone Demo</title>
</head>
<body>
  <h1>Record a Voice Sample</h1>
  <button id="recordBtn">Record</button>
  <button id="stopBtn" disabled>Stop</button>
  <h2>Clone the Voice</h2>
  <button id="cloneBtn" disabled>Clone Voice</button>
  <h2>Synthesize Text</h2>
  <input type="text" id="textInput" placeholder="Type something...">
  <button id="synthBtn" disabled>Synthesize</button>
  <audio id="outputAudio" controls></audio>

  <script>
    let mediaRecorder;
    let audioChunks = [];
    let voiceId = null;

    const recordBtn = document.getElementById('recordBtn');
    const stopBtn = document.getElementById('stopBtn');
    const cloneBtn = document.getElementById('cloneBtn');
    const synthBtn = document.getElementById('synthBtn');
    const outputAudio = document.getElementById('outputAudio');

    recordBtn.onclick = async () => {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      mediaRecorder = new MediaRecorder(stream);
      mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
      mediaRecorder.start();
      recordBtn.disabled = true;
      stopBtn.disabled = false;
    };

    stopBtn.onclick = () => {
      mediaRecorder.stop();
      recordBtn.disabled = false;
      stopBtn.disabled = true;
      cloneBtn.disabled = false;
    };

    cloneBtn.onclick = async () => {
      const blob = new Blob(audioChunks, { type: 'audio/wav' });
      const formData = new FormData();
      formData.append('file', blob, 'sample.wav');

      const res = await fetch('/clone', { method: 'POST', body: formData });
      const data = await res.json();
      voiceId = data.voice_id;
      synthBtn.disabled = false;
      alert('Voice cloned! You can now synthesize text.');
    };

    synthBtn.onclick = async () => {
      const text = document.getElementById('textInput').value;
      const res = await fetch('/synthesize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ voice_id: voiceId, text })
      });
      const data = await res.json();
      const audioBuffer = new Uint8Array(Buffer.from(data.audio, 'hex')).buffer;
      const audioBlob = new Blob([audioBuffer], { type: 'audio/wav' });
      outputAudio.src = URL.createObjectURL(audioBlob);
    };
  </script>
</body>
</html>
```

**Key Points**

```
   uvicorn main:app --reload
```

Place the HTML file in a `public/` folder and serve it with any static server (e.g., `python -m http.server`).

| Issue | Fix | 
|---|---|
| **Long latency** | ElevenLabs processes audio asynchronously. Use a progress indicator or retry logic. | 
| **Audio quality** | Record in a quiet environment, use a decent microphone, and keep the clip under 60 seconds. | 
| **Quota limits** | The free tier is generous but monitor usage via the ElevenLabs dashboard. | 
| **Security** | Never expose your API key on the client. All calls to ElevenLabs should go through your backend. | 
| **Legal** | Ensure you have permission to clone any voice. Voice cloning can raise privacy concerns. | 

Once you’ve proven the concept, consider adding:

Ready to bring your app to life with lifelike voice cloning? Sign up with ElevenLabs at **[https://try.elevenlabs.io/kr07zfuqn1bp](https://try.elevenlabs.io/kr07zfuqn1bp)** and get instant access to powerful APIs that make voice AI a breeze. Happy coding, and may your app speak volumes!
