From 42eccafcacb11c504f10794770768b4ef2303299 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 27 Jan 2026 16:45:24 -0500 Subject: [PATCH] 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 --- public/src/tts-modal.js | 2 +- public/src/tts.js | 38 ++++++++++++++++++---- public/text-to-speech.html | 65 ++++++++++++++++++++------------------ 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/public/src/tts-modal.js b/public/src/tts-modal.js index 1fa37d2..03b6973 100644 --- a/public/src/tts-modal.js +++ b/public/src/tts-modal.js @@ -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); diff --git a/public/src/tts.js b/public/src/tts.js index 087f66e..15710fc 100644 --- a/public/src/tts.js +++ b/public/src/tts.js @@ -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; diff --git a/public/text-to-speech.html b/public/text-to-speech.html index b817439..e0339d7 100644 --- a/public/text-to-speech.html +++ b/public/text-to-speech.html @@ -7,7 +7,7 @@ Open-Source Text-to-Speech | uncloseai-speech | uncloseai.com - + @@ -109,7 +109,7 @@
  • Zero API Keys: No registration, no tracking, no rate limits on your own infrastructure
  • OpenAI-Compatible: Drop-in replacement - change one URL and you're running
  • Five TTS Engines: Qwen3-TTS (default, voice cloning), Piper (fast), XTTS (quality), Silero (CPU-friendly), Kokoro (lightweight)
  • -
  • 20+ Cloned Voices: With 10-language support including Chinese, Japanese, Korean, and European languages
  • +
  • 42+ Built-In Cloned Voices: With 10-language support including Chinese, Japanese, Korean, and European languages
  • Self-Hostable: Docker compose, Makefile-driven, runs on your hardware
  • AGPL v3 Licensed: Keeps TTS libre forever - even network service users get source code
  • @@ -118,21 +118,21 @@

    Test our community-hosted endpoint before self-hosting:

    -
    # Auto-detect model from voice name
    +        
    # 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

    Auto-Detection Magic

    @@ -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(

    Discover Voices

    -

    Extended /v1/voices endpoint shows all 227 voices with engine metadata:

    +

    Browse all available voices with engine metadata: speech.ai.unturf.com/v1/voices

    curl https://speech.ai.unturf.com/v1/voices | jq .

    Discover Models

    -

    Standard OpenAI /v1/models endpoint shows available TTS models:

    +

    See all available TTS engines: speech.ai.unturf.com/v1/models

    curl https://speech.ai.unturf.com/v1/models | jq .
    @@ -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

    That's it. You now have a production TTS API running locally.

    -

    Remote Deployment (Production)

    +

    GPU Deployment (Voice Cloning)

    -

    Deploy to a GPU server for XTTS voice cloning:

    +

    Deploy with GPU support for Qwen3-TTS voice cloning:

    -
    # Configure deployment target
    -cp vars.sh.example vars.sh
    -# Edit vars.sh with your server details
    -
    -# Deploy to remote server (syncs, builds, restarts)
    +        
    # 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
    +# Test the endpoint +make test + +# Test all 42+ voices +make hydrate

    The Five Engines

    @@ -224,7 +225,7 @@ make test @@ -287,6 +288,8 @@ make test