feat: replace MeloTTS with Coqui XTTS-v2 natural Korean voice
Some checks failed
Release / semantic-release (push) Successful in 30s
tests / Unit tests (Linux, Python 3.11) (push) Failing after 5m17s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Release / build-linux (push) Has been cancelled
Some checks failed
Release / semantic-release (push) Successful in 30s
tests / Unit tests (Linux, Python 3.11) (push) Failing after 5m17s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Release / build-linux (push) Has been cancelled
MeloTTS's single Korean speaker sounded non-native ("foreign accent"). Swap it
for Coqui XTTS-v2 with the built-in female studio speaker "Ana Florence"
(language ko), the natural voice used in earlier local runs.
- bridge/xtts_worker.py: new warm HTTP worker (own /opt/xtts venv), same
/synth + /health contract and PCM16 output as the old melo worker
- docker/setup-xtts.sh: builds the venv with cu128 torch (Blackwell) + Coqui
TTS and bakes the XTTS-v2 model offline. Pins transformers>=4.57,<5 (5.x
removed isin_mps_friendly, breaking XTTS) and installs the [codec] extra
(torch>=2.9 needs torchcodec) — both verified by a real host synth
- Dockerfile: replace the melo build layer with the xtts layer
- supervisord.conf: melo-worker -> xtts-worker, env passthrough for
XTTS_DEVICE/SPEAKER/LANGUAGE (always set via compose defaults)
- bridge/server.py: default TTS_ENGINE=xtts, route to the xtts worker, generic
worker-synth helper, neural-only fallback flag (XTTS_FALLBACK_PIPER)
- settings UI: engine dropdown xtts/piper, drop the dead melo_speed field, fix
the supervisorctl restart target to xtts-worker
- compose/.env.example/README: XTTS_* vars, speaker/language knobs, remove melo
- remove bridge/melo_worker.py and docker/setup-melo.sh
- tests: xtts treated as multilingual (not English-only)
Verified on host: coqui-tts loads XTTS-v2 and synthesises Korean as
"Ana Florence" to a 16-bit mono 24kHz WAV.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Install a dedicated MeloTTS (Korean voice) venv at /opt/melo.
|
||||
#
|
||||
# Why a SEPARATE venv (not the brain-bridge /opt/venv):
|
||||
# - MeloTTS pins old deps (transformers 4.27.4 / tokenizers 0.13.3 / fugashi)
|
||||
# whose binary wheels exist only for cp311, so we use python3.11 here even
|
||||
# though the image's default interpreter is 3.12.
|
||||
# - It isolates the heavy torch/transformers stack from the slim bridge env,
|
||||
# which pins numpy<2 for faster-whisper.
|
||||
#
|
||||
# torch is the CUDA (cu128) build so MeloTTS runs on the GPU alongside Ollama +
|
||||
# Whisper. CPU synth serialised under concurrent load (whisper STT + bot) and
|
||||
# blew TTS up to 7-8s per reply; on the GPU a sentence synthesises in ~0.3s.
|
||||
# cu128 is the Blackwell (sm_120) wheel verified on this host's RTX 5050.
|
||||
# The worker selects the device via MELO_DEVICE=cuda (compose).
|
||||
# ============================================================================
|
||||
set -euxo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get update
|
||||
# Build deps for fugashi / mecab-python3 + a system MeCab dict, plus python3.11.
|
||||
apt-get install -y --no-install-recommends \
|
||||
software-properties-common build-essential pkg-config swig \
|
||||
libmecab-dev mecab mecab-ipadic-utf8
|
||||
add-apt-repository -y ppa:deadsnakes/ppa
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends python3.11 python3.11-venv python3.11-dev
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
python3.11 -m venv /opt/melo
|
||||
/opt/melo/bin/pip install --no-cache-dir --upgrade pip wheel setuptools
|
||||
|
||||
# CUDA (cu128) torch first, so MeloTTS's unpinned `torch` dep is already
|
||||
# satisfied with the GPU build. Pinned to the Blackwell-verified versions
|
||||
# (2.11.0+cu128) for reproducible rebuilds.
|
||||
/opt/melo/bin/pip install --no-cache-dir torch==2.11.0+cu128 torchaudio==2.11.0+cu128 \
|
||||
--index-url https://download.pytorch.org/whl/cu128
|
||||
|
||||
# MeloTTS from GitHub. The PyPI sdist is broken (its setup.py reads a
|
||||
# requirements.txt that is not shipped in the sdist), so install from the repo.
|
||||
# Pinned to a commit (not refs/heads/main) so rebuilds are reproducible.
|
||||
/opt/melo/bin/pip install --no-cache-dir \
|
||||
"https://github.com/myshell-ai/MeloTTS/archive/209145371cff8fc3bd60d7be902ea69cbdb7965a.tar.gz"
|
||||
|
||||
# Korean g2p backend. MeloTTS otherwise tries to pip-install this on the first
|
||||
# Korean request, which fails in a network-isolated container at runtime.
|
||||
/opt/melo/bin/pip install --no-cache-dir python-mecab-ko python-mecab-ko-dic
|
||||
|
||||
# Remove the full `unidic` package (its dictionary is never downloaded, only a
|
||||
# stub) so mecab-python3 falls back to the bundled `unidic_lite` dict. Without
|
||||
# this, importing melo's Japanese module fails with a missing-mecabrc error.
|
||||
/opt/melo/bin/pip uninstall -y unidic || true
|
||||
|
||||
# Pre-cache every model asset MeloTTS pulls at runtime, so the worker starts
|
||||
# offline and the first Discord turn pays no download cost. Importing melo.api
|
||||
# fetches the Japanese (tohoku-nlp/bert-base-japanese-v3) and Korean
|
||||
# (kykim/bert-kor-base) BERT tokenizers plus nltk g2p data; loading the KR voice
|
||||
# downloads the OpenVoice KR config+checkpoint, and a real synth pulls the
|
||||
# Korean BERT weights. All of these go through huggingface_hub.
|
||||
#
|
||||
# CRITICAL: at runtime docker-compose mounts the `whisper_cache` named volume
|
||||
# over /root/.cache/huggingface (for faster-whisper). That volume would SHADOW
|
||||
# anything baked into the default HF cache, so we pin the melo worker to a
|
||||
# DEDICATED, non-volume cache dir (/opt/melo-cache) here AND in supervisord, and
|
||||
# warm it once. nltk_data (/root/nltk_data) is not volume-mounted so it stays.
|
||||
export HF_HOME=/opt/melo-cache
|
||||
mkdir -p "$HF_HOME"
|
||||
MELO_LANGUAGE=KR HF_HOME=/opt/melo-cache /opt/melo/bin/python - <<'PY'
|
||||
import tempfile
|
||||
from melo.api import TTS
|
||||
|
||||
model = TTS(language="KR", device="cpu")
|
||||
out = tempfile.mktemp(suffix=".wav")
|
||||
model.tts_to_file("초기화 워밍업입니다.", model.hps.data.spk2id["KR"], out, speed=1.5)
|
||||
print("[setup-melo] warm-up KR synth OK ->", out)
|
||||
PY
|
||||
|
||||
echo "[setup-melo] MeloTTS venv ready at /opt/melo"
|
||||
72
docker/setup-xtts.sh
Normal file
72
docker/setup-xtts.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Install a dedicated Coqui XTTS-v2 (natural Korean voice) venv at /opt/xtts.
|
||||
#
|
||||
# Why a SEPARATE venv (not the brain-bridge /opt/venv or /opt/melo):
|
||||
# - Coqui TTS pulls its own heavy torch/transformers stack; isolating it keeps
|
||||
# the slim bridge env (numpy<2 for faster-whisper) untouched.
|
||||
# - We use python3.11 (installed for the melo layer) because Coqui ships cp311
|
||||
# wheels and torch cu128 is available for it.
|
||||
#
|
||||
# torch is the CUDA (cu128) build so XTTS runs on the GPU alongside Ollama +
|
||||
# Whisper. cu128 is the Blackwell (sm_120) wheel verified on this host.
|
||||
# The worker selects the device via XTTS_DEVICE=cuda (compose).
|
||||
#
|
||||
# XTTS-v2 is non-commercial (Coqui Public Model License). COQUI_TOS_AGREED=1
|
||||
# accepts it non-interactively so the model can load in a headless container.
|
||||
# ============================================================================
|
||||
set -euxo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export COQUI_TOS_AGREED=1
|
||||
|
||||
# Install python3.11 if not already present, so this layer is self-contained.
|
||||
if ! command -v python3.11 >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends software-properties-common
|
||||
add-apt-repository -y ppa:deadsnakes/ppa
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends python3.11 python3.11-venv python3.11-dev
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
|
||||
python3.11 -m venv /opt/xtts
|
||||
/opt/xtts/bin/pip install --no-cache-dir --upgrade pip wheel setuptools
|
||||
|
||||
# CUDA (cu128) torch first so Coqui's `torch` dep is satisfied with the GPU
|
||||
# build. Pinned to the Blackwell-verified versions for reproducible rebuilds.
|
||||
/opt/xtts/bin/pip install --no-cache-dir torch==2.11.0+cu128 torchaudio==2.11.0+cu128 \
|
||||
--index-url https://download.pytorch.org/whl/cu128
|
||||
|
||||
# Coqui TTS (maintained fork; provides the `TTS` package and XTTS-v2). The
|
||||
# [codec] extra pulls torchcodec, which torch >=2.9 requires for audio IO
|
||||
# (without it the import fails with TORCHCODEC_IMPORT_ERROR). torchcodec also
|
||||
# needs the system FFmpeg shared libs, which are present (ffmpeg apt package).
|
||||
/opt/xtts/bin/pip install --no-cache-dir "coqui-tts[codec]"
|
||||
|
||||
# Pin transformers to the 4.57+ / <5 range. coqui-tts requires >=4.57 but does
|
||||
# NOT cap the upper bound, and transformers 5.x removed `isin_mps_friendly`
|
||||
# (used by XTTS's tortoise layer), so an unpinned install pulls 5.x and the
|
||||
# model import fails with "cannot import name 'isin_mps_friendly'". Pin <5.
|
||||
/opt/xtts/bin/pip install --no-cache-dir "transformers>=4.57,<5"
|
||||
|
||||
# Pre-bake the XTTS-v2 model so the worker starts offline and the first Discord
|
||||
# turn pays no download cost. The model is cached under TTS_HOME; we pin a
|
||||
# DEDICATED, non-volume dir (/opt/xtts-cache) AND set it in supervisord, because
|
||||
# runtime volume mounts (whisper_cache over /root/.cache) must not shadow it.
|
||||
export TTS_HOME=/opt/xtts-cache
|
||||
mkdir -p "$TTS_HOME"
|
||||
COQUI_TOS_AGREED=1 TTS_HOME=/opt/xtts-cache XTTS_SPEAKER="Ana Florence" \
|
||||
/opt/xtts/bin/python - <<'PY'
|
||||
import os
|
||||
os.environ["COQUI_TOS_AGREED"] = "1"
|
||||
from TTS.api import TTS
|
||||
|
||||
speaker = os.environ.get("XTTS_SPEAKER", "Ana Florence")
|
||||
model = TTS("tts_models/multilingual/multi-dataset/xtts_v2") # downloads to TTS_HOME
|
||||
out = "/tmp/xtts_warm.wav"
|
||||
model.tts_to_file(text="초기화 워밍업입니다.", speaker=speaker, language="ko", file_path=out)
|
||||
print("[setup-xtts] warm-up KR synth OK ->", out, "speaker:", speaker)
|
||||
PY
|
||||
|
||||
echo "[setup-xtts] Coqui XTTS-v2 venv ready at /opt/xtts (cache /opt/xtts-cache)"
|
||||
@@ -49,25 +49,22 @@ stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:melo-worker]
|
||||
; Warm MeloTTS Korean voice (speed 1.5) in its own py3.11 venv. The bridge's
|
||||
; synthesize() POSTs here; if this is down the bridge falls back to Piper.
|
||||
command=/app/docker/run-if-role.sh full,bot /opt/melo/bin/python /app/bridge/melo_worker.py
|
||||
[program:xtts-worker]
|
||||
; Warm Coqui XTTS-v2 Korean voice (natural female "Ana Florence") in its own
|
||||
; py3.11 venv. The bridge's synthesize() POSTs here; if this is down the bridge
|
||||
; falls back to Piper (English) only when XTTS_FALLBACK_PIPER=1.
|
||||
command=/app/docker/run-if-role.sh full,bot /opt/xtts/bin/python /app/bridge/xtts_worker.py
|
||||
directory=/app
|
||||
; HF_HOME points at the dedicated, image-baked melo cache (warmed in
|
||||
; setup-melo.sh). The brain's whisper_cache volume is mounted over
|
||||
; /root/.cache/huggingface, so without this the pre-cached BERT + KR checkpoint
|
||||
; would be shadowed and re-downloaded (and would fail if the host is offline).
|
||||
; HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE force pure-cache reads: the pinned old
|
||||
; transformers/huggingface_hub otherwise retry the network on every load and
|
||||
; error out instead of falling back to the (complete) baked cache.
|
||||
; MELO_DEVICE and MELO_SPEED inherit from the container env (compose sets both
|
||||
; with defaults: cuda / 1.5) so the worker runs MeloTTS on the GPU at the
|
||||
; configured rate. supervisord interpolates %(ENV_x)s from its own environment,
|
||||
; which is the container's — so MELO_SPEED must always be set in the env
|
||||
; (compose guarantees it) or this expansion fails at startup. Hardcoding 1.5
|
||||
; here previously shadowed the .env value, so lowering MELO_SPEED had no effect.
|
||||
environment=MELO_LANGUAGE="KR",MELO_SPEED="%(ENV_MELO_SPEED)s",MELO_DEVICE="%(ENV_MELO_DEVICE)s",MELO_WORKER_HOST="127.0.0.1",MELO_WORKER_PORT="8770",HF_HOME="/opt/melo-cache",HF_HUB_OFFLINE="1",TRANSFORMERS_OFFLINE="1"
|
||||
; TTS_HOME points at the dedicated, image-baked XTTS cache (warmed in
|
||||
; setup-xtts.sh). The brain's whisper_cache volume is mounted over
|
||||
; /root/.cache, so a dedicated non-volume cache dir avoids the baked model being
|
||||
; shadowed and re-downloaded (which would fail if the host is offline).
|
||||
; XTTS_DEVICE / XTTS_SPEAKER / XTTS_LANGUAGE inherit from the container env
|
||||
; (compose sets them with defaults: cuda / "Ana Florence" / ko). supervisord
|
||||
; interpolates %(ENV_x)s from its own environment, which is the container's — so
|
||||
; these must always be set in the env (compose guarantees it) or this expansion
|
||||
; fails at startup. COQUI_TOS_AGREED accepts the non-commercial XTTS license.
|
||||
environment=XTTS_DEVICE="%(ENV_XTTS_DEVICE)s",XTTS_SPEAKER="%(ENV_XTTS_SPEAKER)s",XTTS_LANGUAGE="%(ENV_XTTS_LANGUAGE)s",XTTS_WORKER_HOST="127.0.0.1",XTTS_WORKER_PORT="8771",TTS_HOME="/opt/xtts-cache",COQUI_TOS_AGREED="1"
|
||||
priority=280
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
|
||||
Reference in New Issue
Block a user