自分のパソコンで動くOpenAI互換のTTSサーバー
多くのアプリはOpenAIの音声APIで話します。Open WebUIやSillyTavern、OpenAI SDKで作られたプログラムは、ベースURLを変えてPOST /v1/audio/speechを別のサーバーに送れます。自分のサーバーを指定すれば、音声は自分のパソコンで無料・オフラインで動きます。
このガイドでは無料のSuperClonicaエンジンを使い、約70行のPythonでそのサーバーをつくります。公式のOpenAI Pythonクライアントで動作を確認済みで、MP3、WAV、生のPCMを返し、任意のキーも確認します。
インストール
Python 3.10以降が必要です。エンジン、Webサーバー、音声エンコーダーをインストールします:
pip install superclonica-cpu fastapi uvicorn av
初回起動時にSupertonic 3モデル(約400 MB)がダウンロードされます。その後は普通のCPUでオフラインで動き、NVIDIAのグラフィックカードは速くするだけです。
サーバー
これを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)}起動するとポート8880で応答します:
uvicorn openai_tts:app --host 127.0.0.1 --port 8880
voiceには標準の10音声(M1〜M5、F1〜F5)のどれかを指定します。知らない名前はF1になるので、alloyを送るクライアントにも応答が返ります。TTS_LANGUAGEはアプリの言語に合わせます。エンジンは31言語を話しますが、中国語にはまだ対応していません。自分以外がポートに届く場合はTTS_KEYを設定してください。
Open WebUIにつなぐ
Admin Panel > Settings > Audioを開きます。Text-to-Speech EngineをOpenAI、API Base URLをhttp://localhost:8880/v1、TTS Modelをsuperclonica、TTS Voiceを標準音声のどれかにします。Open WebUIは空でないAPIキーを求めるので、TTS_KEYか、空にした場合は任意の文字を入れてください。Open WebUIをDockerで動かす場合は、サーバーを--host 0.0.0.0で起動しhttp://host.docker.internal:8880/v1を使います。Linuxではコンテナに--add-host=host.docker.internal:host-gatewayが必要です。Response Splittingはpunctuationのままにすると、長い回答も最初の文から話し始めます。
SillyTavernにつなぐ
Extensions > TTSを開き、プロバイダーにOpenAI Compatibleを選びます。エンドポイントにhttp://localhost:8880/v1/audio/speech、モデルにsuperclonica、キーを入れ、このプロバイダーは音声一覧を取得しないので自分で入力します:M1,M2,M3,M4,M5,F1,F2,F3,F4,F5。そのあと音声マップで各キャラクターと自分のペルソナに声を割り当て、Applyを押します。
キャラクターの声とほかのクライアント
SuperClonicaカタログの声を使うには、その音声ファイルをダウンロードしてVOICES["narrator"] = "/path/to/the/file"のような行を追加します。するとnarratorという名前がSillyTavernの音声一覧を含むすべてのクライアントで使えます。OpenAI SDKで作ったプログラムも使えます。base_urlをhttp://localhost:8880/v1にしてクライアントを作り、いつもどおりaudio.speech.createを呼びます。