ガイド

自分のパソコンで動く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を呼びます。