Replace RVC WebUI with Matcha TTS API - faster TTS solution
Browse files- Dockerfile +17 -26
- README.md +56 -32
- app.py +0 -192
- main.py +84 -0
- requirements.txt +6 -53
Dockerfile
CHANGED
|
@@ -1,35 +1,26 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
# Install system dependencies
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
-
&& git lfs install
|
| 11 |
|
| 12 |
-
#
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
# "latest" on every rebuild silently breaks the Space whenever upstream
|
| 16 |
-
# pushes a change — which is exactly what happened here.
|
| 17 |
-
#
|
| 18 |
-
# To pick a commit: browse https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI/commits/main
|
| 19 |
-
# test it locally/in a scratch Space, then hardcode the hash below.
|
| 20 |
-
ARG RVC_COMMIT=main
|
| 21 |
-
RUN git clone https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI.git /app/rvc \
|
| 22 |
-
&& cd /app/rvc \
|
| 23 |
-
&& git checkout ${RVC_COMMIT}
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
# webui.py code. (The old requirements.txt pinned gradio 3.14 + fairseq,
|
| 28 |
-
# which is the pre-rewrite stack and no longer matches current webui.py.)
|
| 29 |
-
RUN pip install --no-cache-dir pip -U \
|
| 30 |
-
&& pip install --no-cache-dir -r /app/rvc/requirments_cpu_py312.txt
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
# Install system dependencies
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
+
libsndfile1 \
|
| 8 |
+
ffmpeg \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
| 10 |
|
| 11 |
+
# Install Python dependencies
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# Copy application code
|
| 16 |
+
COPY main.py .
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
# Expose port
|
| 19 |
+
EXPOSE 7860
|
| 20 |
|
| 21 |
+
# Health check
|
| 22 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 23 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 24 |
+
|
| 25 |
+
# Run the application
|
| 26 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,53 +1,77 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
#
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
##
|
| 15 |
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
-
|
| 20 |
-
- Supports
|
| 21 |
|
| 22 |
-
##
|
| 23 |
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
| 29 |
|
| 30 |
-
##
|
|
|
|
| 31 |
|
| 32 |
-
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
| 37 |
-
-
|
| 38 |
-
-
|
| 39 |
-
-
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Matcha TTS API
|
| 3 |
+
emoji: 🗣️
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Matcha TTS Space
|
| 12 |
|
| 13 |
+
A Hugging Face Space deployment of **Matcha-TTS** for fast, high-quality text-to-speech synthesis on CPU Basic.
|
| 14 |
|
| 15 |
+
## Features
|
| 16 |
|
| 17 |
+
- **Fast**: Real-time factor (RTF) ~0.1-0.15 on CPU Basic
|
| 18 |
+
- **High Quality**: Natural-sounding speech with good prosody
|
| 19 |
+
- **Simple API**: REST endpoint for TTS synthesis
|
| 20 |
+
- **CPU Optimized**: Uses ONNX runtime for efficient inference
|
| 21 |
+
- **Configurable**: Supports different Matcha-TTS models via environment variable
|
| 22 |
|
| 23 |
+
## API Endpoints
|
| 24 |
|
| 25 |
+
### POST `/tts`
|
| 26 |
+
Synthesize speech from text.
|
| 27 |
|
| 28 |
+
**Request Body:**
|
| 29 |
+
```json
|
| 30 |
+
{
|
| 31 |
+
"text": "Hello world",
|
| 32 |
+
"language": "en",
|
| 33 |
+
"speaker_id": 0,
|
| 34 |
+
"noise_scale": 0.667,
|
| 35 |
+
"noise_scale_w": 0.8,
|
| 36 |
+
"length_scale": 1.0
|
| 37 |
+
}
|
| 38 |
+
```
|
| 39 |
|
| 40 |
+
**Returns:** WAV audio file (24kHz, 16-bit PCM)
|
| 41 |
|
| 42 |
+
### GET `/health`
|
| 43 |
+
Health check endpoint.
|
| 44 |
|
| 45 |
+
### GET `/`
|
| 46 |
+
API information and usage example.
|
| 47 |
|
| 48 |
+
## Usage Example (curl)
|
| 49 |
|
| 50 |
+
```bash
|
| 51 |
+
curl -X POST "https://your-username-your-space.hf.space/tts" \
|
| 52 |
+
-H "Content-Type: application/json" \
|
| 53 |
+
-d '{"text":"Hello, this is Matcha TTS!"}' \
|
| 54 |
+
-o output.wav
|
| 55 |
+
```
|
| 56 |
|
| 57 |
+
## Model
|
| 58 |
+
|
| 59 |
+
Default: `matcha-tts/matcha-v1.0` (English)
|
| 60 |
+
|
| 61 |
+
To use a different model, set the `MATCHA_MODEL` environment variable in Space Settings.
|
| 62 |
|
| 63 |
+
## Performance
|
| 64 |
+
|
| 65 |
+
On Hugging Face CPU Basic:
|
| 66 |
+
- Startup time: ~10-15 seconds (model loading)
|
| 67 |
+
- Synthesis RTF: ~0.1-0.15 (1 second of audio in 100-150ms)
|
| 68 |
+
- Concurrent requests: Limited by CPU, recommend sequential processing
|
| 69 |
+
|
| 70 |
+
## Deployment
|
| 71 |
|
| 72 |
+
This Space is built from:
|
| 73 |
+
- `Dockerfile`: Container definition
|
| 74 |
+
- `requirements.txt`: Python dependencies
|
| 75 |
+
- `main.py`: FastAPI application
|
| 76 |
|
| 77 |
+
Built and deployed automatically on pushes to the main branch.
|
|
|
app.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
import shutil
|
| 4 |
-
import subprocess
|
| 5 |
-
import time
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
|
| 8 |
-
# Hugging Face Spaces / CPU-only environment setup
|
| 9 |
-
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
| 10 |
-
os.environ["GRADIO_SERVER_NAME"] = "0.0.0.0"
|
| 11 |
-
os.environ["GRADIO_SERVER_PORT"] = "7860"
|
| 12 |
-
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
|
| 13 |
-
|
| 14 |
-
ROOT = Path(__file__).resolve().parent
|
| 15 |
-
RVC_DIR = ROOT / "rvc"
|
| 16 |
-
|
| 17 |
-
def run(cmd, cwd=None):
|
| 18 |
-
print(">>>", " ".join(map(str, cmd)), flush=True)
|
| 19 |
-
subprocess.check_call(cmd, cwd=cwd)
|
| 20 |
-
|
| 21 |
-
def download_with_retry(repo_id, filename, local_dir, max_retries=3):
|
| 22 |
-
"""Download a single file with retry logic."""
|
| 23 |
-
from huggingface_hub import hf_hub_download
|
| 24 |
-
for attempt in range(max_retries):
|
| 25 |
-
try:
|
| 26 |
-
print(f"Downloading {filename} from {repo_id} (attempt {attempt+1}/{max_retries})...", flush=True)
|
| 27 |
-
hf_hub_download(
|
| 28 |
-
repo_id=repo_id,
|
| 29 |
-
filename=filename,
|
| 30 |
-
local_dir=local_dir,
|
| 31 |
-
)
|
| 32 |
-
target = Path(local_dir) / filename
|
| 33 |
-
if target.exists() and target.stat().st_size > 1000:
|
| 34 |
-
print(f"✓ {filename} downloaded successfully ({target.stat().st_size} bytes)", flush=True)
|
| 35 |
-
return True
|
| 36 |
-
else:
|
| 37 |
-
print(f"✗ {filename} download failed: file too small or missing", flush=True)
|
| 38 |
-
except Exception as e:
|
| 39 |
-
print(f"✗ Attempt {attempt+1} failed: {e}", flush=True)
|
| 40 |
-
if attempt < max_retries - 1:
|
| 41 |
-
time.sleep(5)
|
| 42 |
-
return False
|
| 43 |
-
|
| 44 |
-
def snapshot_with_retry(repo_id, allow_patterns, local_dir, max_retries=3):
|
| 45 |
-
"""Download a folder/pattern set from the HF repo with retry logic."""
|
| 46 |
-
from huggingface_hub import snapshot_download
|
| 47 |
-
for attempt in range(max_retries):
|
| 48 |
-
try:
|
| 49 |
-
print(f"Downloading {allow_patterns} from {repo_id} (attempt {attempt+1}/{max_retries})...", flush=True)
|
| 50 |
-
snapshot_download(
|
| 51 |
-
repo_id=repo_id,
|
| 52 |
-
allow_patterns=allow_patterns,
|
| 53 |
-
local_dir=local_dir,
|
| 54 |
-
)
|
| 55 |
-
print(f"✓ {allow_patterns} downloaded", flush=True)
|
| 56 |
-
return True
|
| 57 |
-
except Exception as e:
|
| 58 |
-
print(f"✗ Attempt {attempt+1} failed: {e}", flush=True)
|
| 59 |
-
if attempt < max_retries - 1:
|
| 60 |
-
time.sleep(5)
|
| 61 |
-
return False
|
| 62 |
-
|
| 63 |
-
# 1. Guarantee RVC existence regardless of build SDK
|
| 64 |
-
# NOTE: the Dockerfile now pins RVC to a specific commit at build time via
|
| 65 |
-
# RVC_COMMIT. This fallback clone (used if the build step didn't run, e.g.
|
| 66 |
-
# local testing) intentionally does NOT pin a commit, so if you hit weird
|
| 67 |
-
# errors when running app.py standalone, check you're on the same commit
|
| 68 |
-
# as the Docker image.
|
| 69 |
-
if not (RVC_DIR / "webui.py").exists():
|
| 70 |
-
print("RVC missing or incomplete. Performing clean clone...", flush=True)
|
| 71 |
-
if RVC_DIR.exists():
|
| 72 |
-
shutil.rmtree(RVC_DIR, ignore_errors=True)
|
| 73 |
-
|
| 74 |
-
run([
|
| 75 |
-
"git", "clone", "--depth", "1",
|
| 76 |
-
"https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI.git",
|
| 77 |
-
str(RVC_DIR)
|
| 78 |
-
])
|
| 79 |
-
|
| 80 |
-
# 2. Set execution context
|
| 81 |
-
sys.path.insert(0, str(RVC_DIR))
|
| 82 |
-
os.chdir(RVC_DIR)
|
| 83 |
-
|
| 84 |
-
# 3. Ensure asset directories exist (current layout, per upstream README)
|
| 85 |
-
for asset_dir in [
|
| 86 |
-
RVC_DIR / "assets" / "hubert_base",
|
| 87 |
-
RVC_DIR / "assets" / "rmvpe",
|
| 88 |
-
RVC_DIR / "assets" / "weights",
|
| 89 |
-
RVC_DIR / "assets" / "indices",
|
| 90 |
-
RVC_DIR / "assets" / "pymss_weights",
|
| 91 |
-
RVC_DIR / "logs",
|
| 92 |
-
]:
|
| 93 |
-
asset_dir.mkdir(parents=True, exist_ok=True)
|
| 94 |
-
|
| 95 |
-
BASE_MODEL_REPO = "lj1995/VoiceConversionWebUI"
|
| 96 |
-
|
| 97 |
-
# 4. Download base inference assets (this is what the previous app.py was
|
| 98 |
-
# missing entirely — it only ever downloaded the custom voice model).
|
| 99 |
-
print("=== Downloading base RVC inference assets ===", flush=True)
|
| 100 |
-
base_ok = True
|
| 101 |
-
|
| 102 |
-
# HuBERT is now a Transformers-format folder (config.json,
|
| 103 |
-
# preprocessor_config.json, pytorch_model.bin) — NOT the old single
|
| 104 |
-
# fairseq hubert_base.pt file.
|
| 105 |
-
hubert_dir = RVC_DIR / "assets" / "hubert_base"
|
| 106 |
-
if not (hubert_dir / "pytorch_model.bin").exists():
|
| 107 |
-
base_ok &= snapshot_with_retry(
|
| 108 |
-
BASE_MODEL_REPO,
|
| 109 |
-
allow_patterns=["hubert_base/*"],
|
| 110 |
-
local_dir=RVC_DIR / "assets",
|
| 111 |
-
)
|
| 112 |
-
else:
|
| 113 |
-
print("✓ hubert_base already present", flush=True)
|
| 114 |
-
|
| 115 |
-
# RMVPE pitch extraction model
|
| 116 |
-
rmvpe_path = RVC_DIR / "assets" / "rmvpe" / "rmvpe.pt"
|
| 117 |
-
if not (rmvpe_path.exists() and rmvpe_path.stat().st_size > 1000):
|
| 118 |
-
base_ok &= download_with_retry(BASE_MODEL_REPO, "rmvpe.pt", RVC_DIR / "assets" / "rmvpe")
|
| 119 |
-
else:
|
| 120 |
-
print("✓ rmvpe.pt already present", flush=True)
|
| 121 |
-
|
| 122 |
-
# pymss weights — only needed if you use the built-in vocal separation tab.
|
| 123 |
-
# Comment this block out if you don't use that feature, to save build time.
|
| 124 |
-
pymss_dir = RVC_DIR / "assets" / "pymss_weights"
|
| 125 |
-
if not any(pymss_dir.iterdir()) if pymss_dir.exists() else True:
|
| 126 |
-
base_ok &= snapshot_with_retry(
|
| 127 |
-
BASE_MODEL_REPO,
|
| 128 |
-
allow_patterns=["pymss_weights/*"],
|
| 129 |
-
local_dir=RVC_DIR / "assets",
|
| 130 |
-
)
|
| 131 |
-
else:
|
| 132 |
-
print("✓ pymss_weights already present", flush=True)
|
| 133 |
-
|
| 134 |
-
if not base_ok:
|
| 135 |
-
print("⚠ Some base assets failed to download. Inference will likely fail.", flush=True)
|
| 136 |
-
else:
|
| 137 |
-
print("=== Base assets ready ===", flush=True)
|
| 138 |
-
|
| 139 |
-
# 5. Download OldChineseMan RVC model from your model repo
|
| 140 |
-
model_repo = "jacky3102/OldChineseMan_RVC"
|
| 141 |
-
weights_dir = RVC_DIR / "assets" / "weights"
|
| 142 |
-
indices_dir = RVC_DIR / "assets" / "indices"
|
| 143 |
-
|
| 144 |
-
files_to_download = [
|
| 145 |
-
("OldChineseMan.pth", weights_dir),
|
| 146 |
-
("added_IVF24_Flat_nprobe_1_OldChineseMan_v2.index", indices_dir),
|
| 147 |
-
]
|
| 148 |
-
|
| 149 |
-
print("=== Downloading RVC model files ===", flush=True)
|
| 150 |
-
all_ok = True
|
| 151 |
-
for fname, target_dir in files_to_download:
|
| 152 |
-
target = target_dir / fname
|
| 153 |
-
if target.exists() and target.stat().st_size > 1000:
|
| 154 |
-
print(f"✓ {fname} already exists ({target.stat().st_size} bytes)", flush=True)
|
| 155 |
-
else:
|
| 156 |
-
if not download_with_retry(model_repo, fname, target_dir):
|
| 157 |
-
print(f"✗ FAILED to download {fname}", flush=True)
|
| 158 |
-
all_ok = False
|
| 159 |
-
|
| 160 |
-
# Verify model file can be loaded
|
| 161 |
-
if all_ok:
|
| 162 |
-
print("=== Verifying model file ===", flush=True)
|
| 163 |
-
try:
|
| 164 |
-
import torch
|
| 165 |
-
pth_path = weights_dir / "OldChineseMan.pth"
|
| 166 |
-
print(f"Loading {pth_path}...", flush=True)
|
| 167 |
-
state = torch.load(pth_path, map_location="cpu", weights_only=False)
|
| 168 |
-
print(f"✓ Model loaded. Keys: {list(state.keys())[:10]}...", flush=True)
|
| 169 |
-
if "config" in state:
|
| 170 |
-
print(f" Config: {state['config']}", flush=True)
|
| 171 |
-
except Exception as e:
|
| 172 |
-
print(f"✗ Model verification failed: {e}", flush=True)
|
| 173 |
-
all_ok = False
|
| 174 |
-
|
| 175 |
-
if not (all_ok and base_ok):
|
| 176 |
-
print("⚠ Some model/asset files failed verification. Inference may fail.", flush=True)
|
| 177 |
-
else:
|
| 178 |
-
print("=== All model files ready and verified ===", flush=True)
|
| 179 |
-
|
| 180 |
-
# 6. Launch RVC WebUI
|
| 181 |
-
# The current webui.py reads host/port from GRADIO_SERVER_NAME /
|
| 182 |
-
# GRADIO_SERVER_PORT (already set above) rather than a --port flag in the
|
| 183 |
-
# rewritten branch. Run `python webui.py --help` in the build logs once to
|
| 184 |
-
# confirm which flags your pinned commit actually supports, then adjust
|
| 185 |
-
# the args below accordingly. --noautoopen avoids it trying to open a
|
| 186 |
-
# local browser on a headless server.
|
| 187 |
-
print("=== Starting RVC WebUI ===", flush=True)
|
| 188 |
-
run([
|
| 189 |
-
sys.executable,
|
| 190 |
-
"webui.py",
|
| 191 |
-
"--noautoopen",
|
| 192 |
-
], cwd=RVC_DIR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from matcha_tts import MatchaTTS
|
| 4 |
+
import soundfile as sf
|
| 5 |
+
import torch
|
| 6 |
+
import numpy as np
|
| 7 |
+
import os
|
| 8 |
+
import uuid
|
| 9 |
+
from fastapi.responses import Response
|
| 10 |
+
from io import BytesIO
|
| 11 |
+
|
| 12 |
+
app = FastAPI(title="Matcha TTS Space")
|
| 13 |
+
|
| 14 |
+
# Load model on startup
|
| 15 |
+
MODEL_NAME = os.getenv("MATCHA_MODEL", "matcha-tts/matcha-v1.0")
|
| 16 |
+
print(f"Loading Matcha-TTS model: {MODEL_NAME}")
|
| 17 |
+
try:
|
| 18 |
+
model = MatchaTTS.from_pretrained(MODEL_NAME)
|
| 19 |
+
print("Model loaded successfully")
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"Failed to load model: {e}")
|
| 22 |
+
model = None
|
| 23 |
+
|
| 24 |
+
class TTSRequest(BaseModel):
|
| 25 |
+
text: str
|
| 26 |
+
language: str = "en" # ISO 639-1 code
|
| 27 |
+
speaker_id: int = 0
|
| 28 |
+
noise_scale: float = 0.667
|
| 29 |
+
noise_scale_w: float = 0.8
|
| 30 |
+
length_scale: float = 1.0
|
| 31 |
+
|
| 32 |
+
@app.post("/tts")
|
| 33 |
+
async def tts(request: TTSRequest):
|
| 34 |
+
if model is None:
|
| 35 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# Synthesize audio
|
| 39 |
+
audio = model.synthesise(
|
| 40 |
+
request.text,
|
| 41 |
+
speaker_id=request.speaker_id,
|
| 42 |
+
noise_scale=request.noise_scale,
|
| 43 |
+
noise_scale_w=request.noise_scale_w,
|
| 44 |
+
length_scale=request.length_scale,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Convert to numpy array if needed
|
| 48 |
+
if torch.is_tensor(audio):
|
| 49 |
+
audio = audio.cpu().numpy()
|
| 50 |
+
|
| 51 |
+
# Ensure it's 1D array
|
| 52 |
+
if audio.ndim > 1:
|
| 53 |
+
audio = audio.squeeze()
|
| 54 |
+
|
| 55 |
+
# Create WAV in memory
|
| 56 |
+
buffer = BytesIO()
|
| 57 |
+
sf.write(buffer, audio, 24000, format='WAV') # Matcha outputs 24kHz
|
| 58 |
+
buffer.seek(0)
|
| 59 |
+
|
| 60 |
+
return Response(
|
| 61 |
+
content=buffer.read(),
|
| 62 |
+
media_type="audio/wav",
|
| 63 |
+
headers={
|
| 64 |
+
"Content-Disposition": f"attachment; filename=tts_{uuid.uuid4().hex}.wav"
|
| 65 |
+
}
|
| 66 |
+
)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
raise HTTPException(status_code=500, detail=f"Synthesis failed: {str(e)}")
|
| 69 |
+
|
| 70 |
+
@app.get("/health")
|
| 71 |
+
async def health():
|
| 72 |
+
return {"status": "healthy" if model is not None else "model_not_loaded", "model": MODEL_NAME}
|
| 73 |
+
|
| 74 |
+
@app.get("/")
|
| 75 |
+
async def root():
|
| 76 |
+
return {
|
| 77 |
+
"message": "Matcha TTS API",
|
| 78 |
+
"endpoint": "/tts (POST)",
|
| 79 |
+
"model": MODEL_NAME,
|
| 80 |
+
"example": {
|
| 81 |
+
"text": "Hello, this is a test of the Matcha TTS system.",
|
| 82 |
+
"language": "en"
|
| 83 |
+
}
|
| 84 |
+
}
|
requirements.txt
CHANGED
|
@@ -1,53 +1,6 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
setuptools>=75.0,<81
|
| 9 |
-
wheel>=0.45,<1
|
| 10 |
-
packaging>=24.0
|
| 11 |
-
|
| 12 |
-
av>=15.1.0,<16
|
| 13 |
-
einops>=0.8.0,<1
|
| 14 |
-
faiss-cpu>=1.13.0,<2
|
| 15 |
-
ffmpeg-python>=0.2.0,<1
|
| 16 |
-
FreeSimpleGUI>=5.1.0,<5.2
|
| 17 |
-
librosa>=0.10.2,<0.11
|
| 18 |
-
local-attention>=1.11.0,<2
|
| 19 |
-
hyper-connections>=0.1.8,<0.4.3
|
| 20 |
-
matplotlib>=3.8.2,<4
|
| 21 |
-
networkx>=3.2.0,<4
|
| 22 |
-
numpy>=1.26.4,<2
|
| 23 |
-
onnxruntime>=1.24.4,<2
|
| 24 |
-
coloredlogs>=15.0,<16
|
| 25 |
-
opencv-python-headless>=4.10.0,<5
|
| 26 |
-
praat-parselmouth>=0.4.5,<1
|
| 27 |
-
PyYAML>=6.0
|
| 28 |
-
scikit-learn>=1.6.0,<2
|
| 29 |
-
scipy>=1.13.1,<2
|
| 30 |
-
sounddevice>=0.5.0,<1
|
| 31 |
-
soundfile>=0.13.0,<1
|
| 32 |
-
tensorboard>=2.19.0
|
| 33 |
-
torchfcpe>=0.0.4,<0.1
|
| 34 |
-
tqdm>=4.67.0,<5
|
| 35 |
-
transformers>=4.49.0,<4.50
|
| 36 |
-
|
| 37 |
-
# Gradio 3.14 API compatibility (RVC WebUI requires Gradio 3.14.x)
|
| 38 |
-
gradio>=3.14.0,<3.15
|
| 39 |
-
huggingface_hub>=0.24.0
|
| 40 |
-
altair>=4.2.0,<5
|
| 41 |
-
anyio>=3.6.2,<4
|
| 42 |
-
fastapi>=0.88.0,<0.100
|
| 43 |
-
httpx>=0.23.0,<0.24
|
| 44 |
-
markdown-it-py>=2.2.0,<3
|
| 45 |
-
mdit-py-plugins>=0.3.3,<0.4
|
| 46 |
-
pydantic>=1.10.13,<2
|
| 47 |
-
pyparsing>=3.0.9,<3.1
|
| 48 |
-
starlette>=0.22.0,<0.28
|
| 49 |
-
uvicorn>=0.20.0,<0.23
|
| 50 |
-
websockets>=10.4,<11
|
| 51 |
-
|
| 52 |
-
# RVC imports fairseq directly
|
| 53 |
-
fairseq @ git+https://github.com/One-sixth/fairseq.git
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn[standard]==0.30.1
|
| 3 |
+
matcha-tts==0.1.0
|
| 4 |
+
onnxruntime==1.19.2
|
| 5 |
+
soundfile==0.12.1
|
| 6 |
+
pydantic==2.8.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|