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"
}
Endpoints
Text-to-Speech Stream V2
Convert text to speech using Gurubase TTS stream audio response
POST
/
{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.
Setup Instructions:
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
The guru type identifier for the text-to-speech request
Headers
Your API key for authentication. You can obtain your API key from the
Gurubase dashboard.
Body Parameters
The text to convert to speech
Response
The response is a streaming audio file in MP3 format.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/mpegCache-Control: no-cacheX-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 dashboardGURU_SLUG— your guru’s slug (visible in the URL when you open your guru, e.g.my-guru)
- Save the code below as
gurubase-tts.html - Update
API_KEYandGURU_SLUGat the top of the script - Open
gurubase-tts.htmlin your browser
gurubase-tts.html — Full streaming TTS demo
gurubase-tts.html — Full streaming TTS demo
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.Was this page helpful?
⌘I