Update TTS page: 42+ voices, new voice names, API endpoint links

- Update all examples from "alloy" to real voice names (aria, atlas)
- Add clickable links to /v1/voices and /v1/models endpoints
- Update voice count from 20+ to 42+ built-in cloned voices
- Bump TTS modal cache version to 4
- Update deployment instructions (remove vars.sh, add deploy-cpu)
- Add in-memory TTS cache for replay without regeneration
This commit is contained in:
russell@unturf.com 2026-01-27 16:45:24 -05:00
parent 017201b5c9
commit 42eccafcac
3 changed files with 67 additions and 38 deletions

View file

@ -11,7 +11,7 @@ async function populateVoiceDropdown(voiceSelection) {
const cacheTimeKey = 'uncloseai_voices_cache_time';
const cacheVersionKey = 'uncloseai_voices_cache_version';
const cacheExpiration = 60000; // 1 minute in milliseconds
const currentVersion = '3'; // Increment this when voice models change (v3: 20 voices)
const currentVersion = '4'; // Increment this when voice models change (v4: 42+ built-in cloned voices)
try {
const cachedTime = localStorage.getItem(cacheTimeKey);

View file

@ -35,6 +35,9 @@ function detectAudioFormat() {
const AUDIO_FORMAT = detectAudioFormat();
// In-memory TTS cache - keyed by text+voice+model, cleared when tab closes
const ttsCache = new Map();
// Simple JavaScript text cleaning for TTS
function cleanTextForTTS(text) {
return text
@ -457,20 +460,43 @@ export async function generateTitleForTTS(text) {
}
// Handle TTS - respects streaming mode setting
// In streaming mode, returns immediately with audio playing and a `done` promise
// In buffered mode, waits for full download then returns
// Caches audio blobs in memory so replay doesn't regenerate
// Cache is keyed by text+voice+model and clears when the tab closes
export async function handleTTS(text, voice = "alloy", rate = 0.9, model = "tts-1-qwen") {
try {
const cacheKey = `${model}:${voice}:${text}`;
// Check cache - return cached blob as a new Audio element
if (ttsCache.has(cacheKey)) {
console.log("handleTTS: cache hit, replaying");
const cached = ttsCache.get(cacheKey);
const audioUrl = URL.createObjectURL(cached.blob);
const audio = new Audio(audioUrl);
audio.playbackRate = rate;
setLastTTS(text, { audio, blob: cached.blob });
return { audio, blob: cached.blob, blobUrl: audioUrl, streamed: false, done: Promise.resolve({ blob: cached.blob, blobUrl: audioUrl }) };
}
const mode = getTTSMode();
console.log(`handleTTS: using ${mode} mode`);
let result;
if (mode === "streaming") {
return await speakTextStreaming(text, voice, rate, model);
result = await speakTextStreaming(text, voice, rate, model);
} else {
const result = await speakTextDirect(text, voice, rate, model);
// Add done: already-resolved promise for consistent interface
result = await speakTextDirect(text, voice, rate, model);
result.done = Promise.resolve({ blob: result.blob, blobUrl: result.blobUrl });
return result;
}
// Cache the blob once available
if (result.done) {
result.done.then(({ blob }) => {
if (blob) {
ttsCache.set(cacheKey, { blob });
}
}).catch(() => {});
}
return result;
} catch (error) {
console.error("Error in handleTTS:", error);
throw error;

View file

@ -7,7 +7,7 @@
<meta name="theme-color" content="#43a047">
<meta name="color-scheme" content="light dark">
<title>Open-Source Text-to-Speech | uncloseai-speech | uncloseai.com</title>
<meta name="description" content="Self-hostable OpenAI-compatible text-to-speech API with 5 TTS engines, 20+ cloned voices, and zero API keys. Default: Qwen3-TTS with voice cloning.">
<meta name="description" content="Self-hostable OpenAI-compatible text-to-speech API with 5 TTS engines, 42+ built-in cloned voices, and zero API keys. Default: Qwen3-TTS with voice cloning.">
<!-- PicoCSS -->
<link rel="stylesheet" href="/css/pico.classless.min.css">
@ -109,7 +109,7 @@
<li><strong>Zero API Keys:</strong> No registration, no tracking, no rate limits on your own infrastructure</li>
<li><strong>OpenAI-Compatible:</strong> Drop-in replacement - change one URL and you're running</li>
<li><strong>Five TTS Engines:</strong> Qwen3-TTS (default, voice cloning), Piper (fast), XTTS (quality), Silero (CPU-friendly), Kokoro (lightweight)</li>
<li><strong>20+ Cloned Voices:</strong> With 10-language support including Chinese, Japanese, Korean, and European languages</li>
<li><strong>42+ Built-In Cloned Voices:</strong> With 10-language support including Chinese, Japanese, Korean, and European languages</li>
<li><strong>Self-Hostable:</strong> Docker compose, Makefile-driven, runs on your hardware</li>
<li><strong>AGPL v3 Licensed:</strong> Keeps TTS libre forever - even network service users get source code</li>
</ul>
@ -118,21 +118,21 @@
<p>Test our community-hosted endpoint before self-hosting:</p>
<pre><code class="bash"># Auto-detect model from voice name
<pre><code class="bash"># Female voice (aria) - auto-detects Qwen3-TTS engine
curl https://speech.ai.unturf.com/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Welcome to uncloseai-speech. Four TTS engines, zero API keys.",
"voice": "alloy"
"input": "Welcome to uncloseai-speech. Five TTS engines, 42 voices, zero API keys.",
"voice": "aria"
}' > output.mp3
# Explicitly specify model (tts-1-qwen, tts-1, tts-1-hd, tts-1-silero, tts-1-kokoro)
# Male voice (atlas) - explicitly specify Qwen3-TTS model
curl https://speech.ai.unturf.com/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "tts-1-qwen",
"input": "Qwen3-TTS voice cloning with 20 voices.",
"voice": "alloy"
"input": "Qwen3-TTS voice cloning with 42 built-in voices.",
"voice": "atlas"
}' > output-qwen.mp3</code></pre>
<h3 id="voice-auto-detection">Auto-Detection Magic</h3>
@ -148,9 +148,14 @@ client = OpenAI(
# Auto-detects the right engine for each voice
client.audio.speech.create(
voice="alloy", # → Piper (fast CPU)
input="Fast synthesis on CPU"
).stream_to_file("piper.mp3")
voice="aria", # → Qwen3-TTS (female, voice cloning)
input="Voice cloning with 42 built-in voices"
).stream_to_file("qwen.mp3")
client.audio.speech.create(
voice="atlas", # → Qwen3-TTS (male, voice cloning)
input="Male voice synthesis"
).stream_to_file("qwen-male.mp3")
client.audio.speech.create(
voice="en_50", # → Silero (native voice name)
@ -164,13 +169,13 @@ client.audio.speech.create(
<h3 id="discover-voices">Discover Voices</h3>
<p>Extended <code>/v1/voices</code> endpoint shows all 227 voices with engine metadata:</p>
<p>Browse all available voices with engine metadata: <a href="https://speech.ai.unturf.com/v1/voices" target="_blank"><code>speech.ai.unturf.com/v1/voices</code></a></p>
<pre><code class="bash">curl https://speech.ai.unturf.com/v1/voices | jq .</code></pre>
<h3 id="discover-models">Discover Models</h3>
<p>Standard OpenAI <code>/v1/models</code> endpoint shows available TTS models:</p>
<p>See all available TTS engines: <a href="https://speech.ai.unturf.com/v1/models" target="_blank"><code>speech.ai.unturf.com/v1/models</code></a></p>
<pre><code class="bash">curl https://speech.ai.unturf.com/v1/models | jq .</code></pre>
@ -187,13 +192,10 @@ cd uncloseai-speech
# See all available commands
make help
# Deploy locally with Docker
make local-deploy
# Deploy with Docker (GPU)
make deploy
# Download voice models (Piper + XTTS samples)
make voices
# Test it
# Test it (Qwen3-TTS model downloads automatically on first use)
make test
# Watch logs
@ -201,22 +203,21 @@ make logs</code></pre>
<p><strong>That's it.</strong> You now have a production TTS API running locally.</p>
<h3 id="remote-deployment">Remote Deployment (Production)</h3>
<h3 id="gpu-deployment">GPU Deployment (Voice Cloning)</h3>
<p>Deploy to a GPU server for XTTS voice cloning:</p>
<p>Deploy with GPU support for Qwen3-TTS voice cloning:</p>
<pre><code class="bash"># Configure deployment target
cp vars.sh.example vars.sh
# Edit vars.sh with your server details
# Deploy to remote server (syncs, builds, restarts)
<pre><code class="bash"># Build and start with GPU (requires NVIDIA Docker runtime)
make deploy
# Download all voice models
make voices
# Or CPU-only (slower, but works anywhere)
make deploy-cpu
# Test remote endpoint
make test</code></pre>
# Test the endpoint
make test
# Test all 42+ voices
make hydrate</code></pre>
<h2 id="the-engines">The Five Engines</h2>
@ -224,7 +225,7 @@ make test</code></pre>
<ul>
<li><strong>Quality:</strong> State-of-the-art voice cloning from 3-second samples</li>
<li><strong>Languages:</strong> 10 languages (Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian)</li>
<li><strong>Voices:</strong> 20 cloned voices (alloy, echo, fable, onyx, nova, shimmer + 14 extended)</li>
<li><strong>Voices:</strong> 42+ built-in cloned voices (aria, clara, elena, atlas, caleb, felix, and <a href="https://speech.ai.unturf.com/v1/voices" target="_blank">more</a>)</li>
<li><strong>Use Case:</strong> Default engine for all TTS — high quality with fast 97ms first-packet latency</li>
<li><strong>Tech:</strong> 1.7B params, Apache 2.0 license, actively maintained by Alibaba</li>
</ul>
@ -287,6 +288,8 @@ make test</code></pre>
<ul>
<li><strong>Repository:</strong> <a href="https://git.unturf.com/engineering/unturf/uncloseai-speech" target="_blank">git.unturf.com/engineering/unturf/uncloseai-speech</a></li>
<li><strong>API Endpoint:</strong> <code>https://speech.ai.unturf.com/v1</code></li>
<li><strong>Voices:</strong> <a href="https://speech.ai.unturf.com/v1/voices" target="_blank">speech.ai.unturf.com/v1/voices</a></li>
<li><strong>Models:</strong> <a href="https://speech.ai.unturf.com/v1/models" target="_blank">speech.ai.unturf.com/v1/models</a></li>
<li><strong>Documentation:</strong> See <code>docs/</code> directory in repository</li>
<li><strong>Community:</strong> Comments section below, or open an issue on GitLab</li>
</ul>