Skip to main content
POST
https://api.gurubase.io/api/v1/
/
{guru_type}
/
text-to-speech-v2
/
stream
curl --request POST \
  --url https://api.gurubase.io/api/v1/{guru_slug}/text-to-speech-v2/stream/ \  
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "text": "Hello, this is a test message using Gurubase text to speech"
  }'
{
  "msg": "No text provided"
}
Convert text to speech using advanced TTS providers and stream the audio response. Code blocks and inline code are automatically processed for better speech generation.
This feature requires special activation. Please contact our support team to enable TTS for your account.
curl --request POST \
  --url https://api.gurubase.io/api/v1/{guru_slug}/text-to-speech-v2/stream/ \  
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "text": "Hello, this is a test message using Gurubase text to speech"
  }'

Path Parameters

guru_type
string
required
The guru type identifier for the text-to-speech request

Headers

x-api-key
string
required
Your API key for authentication. You can obtain your API key from the Gurubase dashboard.

Body Parameters

text
string
required
The text to convert to speech

Response

The response is a streaming audio file in MP3 format.
audio_stream
audio/mpeg
Streaming audio response in MP3 format
{
  "msg": "No text provided"
}

Streaming Response

The endpoint returns a streaming audio response with the following headers:
  • Content-Type: audio/mpeg
  • Cache-Control: no-cache
  • X-Accel-Buffering: no (disables nginx buffering for real-time streaming)

Code Examples

The following examples show how to implement streaming TTS in your web application. These are complete, working examples that you can use immediately.

Streaming the TTS

This is a single self-contained HTML file that streams audio playback in real time without waiting for the entire response to finish. Save it, update the configuration, and open it in your browser.
Before running the example, you must update two constants at the top of the <script> section:
  • API_KEY — your API key from the Gurubase dashboard
  • GURU_SLUG — your guru’s slug (visible in the URL when you open your guru, e.g. my-guru)
The example will not work with the placeholder values.
Setup Instructions:
  1. Save the code below as gurubase-tts.html
  2. Update API_KEY and GURU_SLUG at the top of the script
  3. Open gurubase-tts.html in your browser
gurubase-tts.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Gurubase TTS Demo</title>
    <script>
      // =============================================================
      //  CONFIGURATION — Update these two values before running
      // =============================================================
      const API_KEY = "your-api-key-here"; // Get from https://app.gurubase.io/api-keys
      const GURU_SLUG = "your-guru-slug"; // Your guru slug, e.g. "my-guru"

      const BASE_URL = "https://api.gurubase.io/api/v1";
      const API_URL = `${BASE_URL}/${GURU_SLUG}/text-to-speech-v2/stream/`;
    </script>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        font-family:
          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
          Cantarell, sans-serif;
        max-width: 600px;
        margin: 0 auto;
        padding: 40px 20px;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        min-height: 100vh;
        color: #333;
      }

      .container {
        background: #fff;
        border-radius: 16px;
        padding: 32px;
        box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
      }

      h1 {
        text-align: center;
        margin: 0 0 32px;
        color: #2d3748;
        font-weight: 600;
        font-size: 28px;
      }

      .input-group {
        margin-bottom: 24px;
      }

      label {
        display: block;
        margin-bottom: 8px;
        font-weight: 500;
        color: #4a5568;
      }

      textarea {
        width: 100%;
        padding: 12px 16px;
        border: 2px solid #e2e8f0;
        border-radius: 12px;
        font-size: 16px;
        font-family: inherit;
        resize: vertical;
        min-height: 120px;
        transition:
          border-color 0.2s,
          box-shadow 0.2s;
      }
      textarea:focus {
        outline: none;
        border-color: #667eea;
        box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
      }

      .controls {
        display: flex;
        gap: 12px;
        align-items: center;
        margin-bottom: 24px;
      }

      .btn {
        padding: 12px 24px;
        border: none;
        border-radius: 12px;
        font-size: 16px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s;
        display: inline-flex;
        align-items: center;
        gap: 8px;
      }
      .btn:disabled {
        opacity: 0.6;
        cursor: not-allowed;
      }

      .btn-primary {
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: #fff;
      }
      .btn-primary:hover:not(:disabled) {
        transform: translateY(-2px);
        box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
      }

      .btn-secondary {
        background: #f7fafc;
        color: #4a5568;
        border: 2px solid #e2e8f0;
      }
      .btn-secondary:hover:not(:disabled) {
        background: #edf2f7;
        border-color: #cbd5e0;
      }

      .status {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        font-size: 14px;
        color: #718096;
        font-weight: 500;
      }
      .status.loading {
        color: #667eea;
      }
      .status.error {
        color: #e53e3e;
      }

      .spinner {
        width: 16px;
        height: 16px;
        border: 2px solid #e2e8f0;
        border-top-color: #667eea;
        border-radius: 50%;
        animation: spin 1s linear infinite;
      }
      @keyframes spin {
        to {
          transform: rotate(360deg);
        }
      }

      .audio-container {
        background: #f7fafc;
        border-radius: 12px;
        padding: 16px;
        margin-top: 16px;
      }
      audio {
        width: 100%;
      }

      .error-message {
        background: #fed7d7;
        color: #c53030;
        padding: 12px 16px;
        border-radius: 8px;
        margin-bottom: 16px;
        font-size: 14px;
        display: none;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Text to Speech Demo — Gurubase</h1>

      <div id="errorMessage" class="error-message"></div>

      <div class="input-group">
        <label for="text">Enter text to convert to speech:</label>
        <textarea id="text" placeholder="Type your text here...">
Hello, this is an example for streaming TTS using the Gurubase API.</textarea
        >
      </div>

      <div class="controls">
        <button id="generateBtn" class="btn btn-primary">
          🎵 Generate Speech
        </button>
        <button id="stopBtn" class="btn btn-secondary" style="display:none">
          ⏹️ Stop
        </button>
        <div id="status" class="status"></div>
      </div>

      <div class="audio-container" id="audioContainer" style="display:none">
        <audio id="tts" controls></audio>
      </div>
    </div>

    <script>
      // =============================================================
      //  DOM Elements
      // =============================================================
      const audioEl = document.getElementById("tts");
      const generateBtn = document.getElementById("generateBtn");
      const stopBtn = document.getElementById("stopBtn");
      const textEl = document.getElementById("text");
      const statusEl = document.getElementById("status");
      const errorMessageEl = document.getElementById("errorMessage");
      const audioContainer = document.getElementById("audioContainer");

      // =============================================================
      //  State
      // =============================================================
      let abortCtrl = null;
      let mediaSource = null;
      let sourceBuffer = null;
      let reader = null;
      let queue = [];
      let isGenerating = false;

      // Feature-detect MediaSource Extensions (unsupported on Safari iOS)
      const useMediaSource =
        typeof window.MediaSource !== "undefined" &&
        MediaSource.isTypeSupported("audio/mpeg");

      // =============================================================
      //  UI Helpers
      // =============================================================
      function showError(message) {
        errorMessageEl.textContent = message;
        errorMessageEl.style.display = "block";
        setStatus("error", "Error occurred");
      }

      function hideError() {
        errorMessageEl.style.display = "none";
      }

      function setStatus(type = "", message = "") {
        statusEl.className = `status ${type}`;
        if (type === "loading") {
          statusEl.innerHTML = `<div class="spinner"></div> ${message}`;
        } else {
          statusEl.textContent = message;
        }
      }

      function updateButtons(generating) {
        isGenerating = generating;
        generateBtn.disabled = generating;
        generateBtn.innerHTML = generating
          ? "⏳ Generating..."
          : "🎵 Generate Speech";
        stopBtn.style.display = generating ? "inline-flex" : "none";
      }

      /**
       * Clean up the current audio session.
       * @param {boolean} skipUIReset – true when called from generateSpeech()
       *   so the button / status state is not immediately overwritten.
       */
      function resetAudio(skipUIReset = false) {
        try {
          audioEl.pause();
        } catch {}

        if (abortCtrl) {
          try {
            abortCtrl.abort();
          } catch {}
        }

        if (mediaSource && mediaSource.readyState === "open") {
          try {
            mediaSource.endOfStream();
          } catch {}
        }

        // Revoke the old blob URL to prevent memory leaks
        const oldSrc = audioEl.src;
        audioEl.removeAttribute("src");
        audioEl.load();
        if (oldSrc && oldSrc.startsWith("blob:")) {
          URL.revokeObjectURL(oldSrc);
        }

        abortCtrl = null;
        mediaSource = null;
        sourceBuffer = null;
        reader = null;
        queue = [];

        if (!skipUIReset) {
          updateButtons(false);
          setStatus("", "");
          audioContainer.style.display = "none";
        }
      }

      // =============================================================
      //  MediaSource & Streaming
      // =============================================================

      /** Set up a MediaSource (or resolve immediately for the blob fallback). */
      function ensureMediaSource() {
        if (!useMediaSource) return Promise.resolve();

        return new Promise((resolve, reject) => {
          mediaSource = new MediaSource();
          audioEl.src = URL.createObjectURL(mediaSource);
          mediaSource.addEventListener(
            "sourceopen",
            () => {
              try {
                sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg");
                sourceBuffer.mode = "sequence";
                sourceBuffer.addEventListener("updateend", drainQueue);
                resolve();
              } catch (e) {
                reject(e);
              }
            },
            { once: true },
          );
        });
      }

      /** Flush queued buffers into the SourceBuffer when it becomes idle. */
      function drainQueue() {
        if (queue.length && !sourceBuffer.updating) {
          sourceBuffer.appendBuffer(queue.shift());
        } else if (
          !isGenerating &&
          !sourceBuffer.updating &&
          mediaSource?.readyState === "open"
        ) {
          try {
            mediaSource.endOfStream();
            setStatus("", "Ready to play");
          } catch {}
        }
      }

      /**
       * Safely copy the relevant slice of the underlying ArrayBuffer.
       * The Uint8Array from ReadableStream may be a view into a larger
       * shared buffer, so we must slice to the exact byte range.
       */
      function safeBuffer(value) {
        return value.buffer.slice(
          value.byteOffset,
          value.byteOffset + value.byteLength,
        );
      }

      /** Open a fetch stream to the TTS endpoint. */
      async function startNetworkStream(text) {
        abortCtrl = new AbortController();

        const resp = await fetch(API_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-API-Key": API_KEY,
          },
          body: JSON.stringify({ text }),
          signal: abortCtrl.signal,
        });

        if (!resp.ok) {
          const errorText = await resp.text().catch(() => "Unknown error");
          throw new Error(`API Error (${resp.status}): ${errorText}`);
        }

        if (!resp.body) {
          throw new Error("No response body received from API");
        }

        reader = resp.body.getReader();
        setStatus("loading", "Streaming audio...");

        pump().catch((err) => {
          if (err.name !== "AbortError") {
            showError(`Streaming error: ${err.message}`);
          }
        });
      }

      /**
       * Read chunks from the network stream and either:
       *  - append them to the MediaSource SourceBuffer (real-time playback), or
       *  - collect them into an array and create a Blob URL when done (fallback).
       */
      async function pump() {
        const blobChunks = useMediaSource ? null : [];

        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            if (value && value.byteLength) {
              const buf = safeBuffer(value);

              if (!useMediaSource) {
                blobChunks.push(buf);
                continue;
              }

              if (!sourceBuffer || sourceBuffer.updating || queue.length) {
                queue.push(buf);
              } else {
                try {
                  sourceBuffer.appendBuffer(buf);
                } catch {
                  queue.push(buf);
                }
              }
            }
          }
        } catch (e) {
          if (e.name !== "AbortError") throw e;
        } finally {
          updateButtons(false);

          // Blob fallback: build audio from collected chunks
          if (!useMediaSource) {
            if (blobChunks && blobChunks.length) {
              const blob = new Blob(blobChunks, { type: "audio/mpeg" });
              audioEl.src = URL.createObjectURL(blob);
              audioContainer.style.display = "block";
              setStatus("", "Audio ready");
              audioEl.play().catch(() => setStatus("", "Click play to listen"));
            }
            return;
          }

          // MediaSource path: signal end of stream
          if (
            sourceBuffer &&
            !sourceBuffer.updating &&
            mediaSource?.readyState === "open"
          ) {
            try {
              mediaSource.endOfStream();
              setStatus("", "Audio ready");
            } catch {}
          }
        }
      }

      // =============================================================
      //  Main Actions
      // =============================================================
      async function generateSpeech() {
        if (isGenerating) return;

        const text = textEl.value.trim();
        if (!text) {
          showError("Please enter some text to convert to speech");
          return;
        }

        hideError();
        updateButtons(true);
        setStatus("loading", "Preparing audio...");

        try {
          resetAudio(true);
          await ensureMediaSource();
          await startNetworkStream(text);

          audioContainer.style.display = "block";
          await audioEl.play().catch(() => {
            setStatus("", "Click play to listen");
          });
        } catch (error) {
          showError(error.message);
          updateButtons(false);
        }
      }

      function stopGeneration() {
        if (!isGenerating) return;
        try {
          if (abortCtrl) abortCtrl.abort();
        } catch {}
        updateButtons(false);
        setStatus("", "Stopped");
      }

      // =============================================================
      //  Bootstrap
      // =============================================================
      generateBtn.addEventListener("click", generateSpeech);
      stopBtn.addEventListener("click", stopGeneration);
      textEl.focus();
    </script>
  </body>
</html>
Testing Tips: - CORS errors? Serve the file from a local web server (e.g. npx serve .) instead of opening gurubase-tts.html directly via file://. - Safari / iOS: The example automatically falls back to a non-streaming blob approach on browsers that do not support the MediaSource Extensions API. - Audio begins playing as soon as the first chunks arrive from the server. - Open the browser console to inspect any errors if audio does not play.