An OpenAI-compatible TTS server on your own computer
Many apps speak through the OpenAI speech API: Open WebUI, SillyTavern and every program built on the OpenAI SDK let you change the base URL and send POST /v1/audio/speech to another server. Point them at a server of your own and their voices run on your computer, for free and offline.
This guide builds that server in about 70 lines of Python with the free SuperClonica engine. We tested it with the official OpenAI Python client: it returns MP3, WAV or raw PCM, and checks an optional key.
Install
You need Python 3.10 or newer. Install the engine, the web server and the audio encoder:
pip install superclonica-cpu fastapi uvicorn av
On the first run the engine downloads the Supertonic 3 model, about 400 MB; after that it works offline on an ordinary processor, and an NVIDIA graphics card only makes it faster.
The server
Save this as openai_tts.py:
import io, os, wave
import av
import numpy as np
from fastapi import FastAPI, Header, HTTPException
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import Response
from pydantic import BaseModel
from superclonica import Superclonica
VOICES = {name: name for name in ["M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"]}
# a catalog voice: VOICES["narrator"] = "/path/to/voice-file-from-the-catalog"
LANGUAGE = os.environ.get("TTS_LANGUAGE", "en")
KEY = os.environ.get("TTS_KEY", "") # leave empty on a private machine
app = FastAPI()
tts = Superclonica()
class Speech(BaseModel):
input: str
voice: str = "F1"
model: str = "superclonica"
response_format: str = "mp3"
def to_wav(pcm, rate):
buffer = io.BytesIO()
with wave.open(buffer, "wb") as file:
file.setnchannels(1)
file.setsampwidth(2)
file.setframerate(rate)
file.writeframes(pcm.tobytes())
return buffer.getvalue()
def to_mp3(pcm, rate):
buffer = io.BytesIO()
with av.open(buffer, "w", format="mp3") as container:
stream = container.add_stream("libmp3lame", rate=rate, layout="mono")
frame = av.AudioFrame.from_ndarray(pcm.reshape(1, -1), format="s16", layout="mono")
frame.rate = rate
for packet in stream.encode(frame):
container.mux(packet)
for packet in stream.encode(None):
container.mux(packet)
return buffer.getvalue()
def speak(request):
voice = VOICES.get(request.voice, "F1")
audio, rate = tts.synthesize(request.input, voice=voice, lang=LANGUAGE)
pcm = (np.clip(np.asarray(audio, dtype=np.float32).reshape(-1), -1, 1) * 32767).astype(np.int16)
if request.response_format == "wav":
return to_wav(pcm, rate), "audio/wav"
if request.response_format == "pcm":
return pcm.tobytes(), "audio/pcm"
return to_mp3(pcm, rate), "audio/mpeg"
@app.post("/v1/audio/speech")
async def speech(request: Speech, authorization: str = Header("")):
if KEY and authorization != "Bearer " + KEY:
raise HTTPException(401)
audio, media_type = await run_in_threadpool(speak, request)
return Response(audio, media_type=media_type)
@app.get("/v1/models")
def models():
return {"object": "list", "data": [{"id": "superclonica", "object": "model"}]}
@app.get("/v1/audio/voices")
def voices():
return {"voices": list(VOICES)}Start it, and it answers on port 8880:
uvicorn openai_tts:app --host 127.0.0.1 --port 8880
The voice field takes one of the ten stock voices, M1 to M5 and F1 to F5; unknown names fall back to F1, so a client that sends alloy still gets an answer. Set TTS_LANGUAGE to the language your app talks in: the engine speaks 31 languages, but not Chinese yet. Set TTS_KEY if anyone but you can reach the port.
Connect Open WebUI
Open Admin Panel, Settings, Audio. Set the Text-to-Speech Engine to OpenAI, the API Base URL to http://localhost:8880/v1, the TTS Model to superclonica and the TTS Voice to one of the stock voices. Open WebUI wants a non-empty API key: type your TTS_KEY, or any text if you left it empty. If Open WebUI runs in Docker, start the server with --host 0.0.0.0 and use http://host.docker.internal:8880/v1; on Linux the container needs --add-host=host.docker.internal:host-gateway. Leave Response Splitting on punctuation so long answers start speaking after the first sentence.
Connect SillyTavern
Open Extensions, TTS, and choose the OpenAI Compatible provider. Enter http://localhost:8880/v1/audio/speech as the endpoint, superclonica as the model, your key, and type the voice list yourself, because this provider does not fetch it: M1,M2,M3,M4,M5,F1,F2,F3,F4,F5. Then give every character and your persona their own voice in the voice map and click Apply.
Character voices and other clients
To use a voice from the SuperClonica catalog, download its voice file and add a line such as VOICES["narrator"] = "/path/to/the/file"; the name narrator then works in every client, including the voice list in SillyTavern. Any program built on the OpenAI SDK works too: create the client with base_url set to http://localhost:8880/v1 and call audio.speech.create as usual.