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 Gurubase TTS stream audio response
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.Documentation Index
Fetch the complete documentation index at: https://docs.gurubase.ai/llms.txt
Use this file to discover all available pages before exploring further.
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"
}
Content-Type: audio/mpegCache-Control: no-cacheX-Accel-Buffering: no (disables nginx buffering for real-time streaming)<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)gurubase-tts.htmlAPI_KEY and GURU_SLUG at the top of the scriptgurubase-tts.html in your browsergurubase-tts.html — Full streaming TTS demo
<!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>
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?