Jacky2305 commited on
Commit
98ec627
·
1 Parent(s): 11b08c9

Switch to Piper TTS - lightweight, fast, multilingual

Browse files
Files changed (3) hide show
  1. Dockerfile +15 -1
  2. main.py +36 -50
  3. requirements.txt +1 -2
Dockerfile CHANGED
@@ -5,13 +5,27 @@ WORKDIR /app
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
 
 
5
  # Install system dependencies
6
  RUN apt-get update && apt-get install -y \
7
  libsndfile1 \
8
+ espeak-ng \
9
+ wget \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
  # Install Python dependencies
13
  COPY requirements.txt .
14
  RUN pip install --no-cache-dir -r requirements.txt
15
 
16
+ # Pre-download voice models at build time (directly to /app)
17
+ RUN cd /app && \
18
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx && \
19
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx.json && \
20
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx && \
21
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json && \
22
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx && \
23
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx.json && \
24
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx && \
25
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx.json && \
26
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx && \
27
+ wget -q https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx.json
28
+
29
  # Copy application code
30
  COPY main.py .
31
 
main.py CHANGED
@@ -1,84 +1,70 @@
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
  }
 
1
  from fastapi import FastAPI, HTTPException
2
  from pydantic import BaseModel
3
+ from piper import PiperVoice
4
  import soundfile as sf
 
 
5
  import os
6
  import uuid
7
  from fastapi.responses import Response
8
  from io import BytesIO
9
 
10
+ app = FastAPI(title="Piper TTS Space")
11
 
12
+ # Load voices on startup (download if needed)
13
+ VOICE_MODELS = {
14
+ "zh": "zh_CN-huayan-medium", # Chinese
15
+ "en": "en_US-lessac-medium", # English
16
+ "ms": "ms_MY-sirius-medium", # Malay
17
+ "ja": "ja_JP-kenji-medium", # Japanese
18
+ "ko": "ko_KR-hui-hao-medium", # Korean
19
+ }
20
+
21
+ VOICES = {}
22
+
23
+ print("Loading Piper voices...")
24
+ for lang, model_name in VOICE_MODELS.items():
25
+ try:
26
+ voice = PiperVoice.load(f"{model_name}.onnx")
27
+ VOICES[lang] = voice
28
+ print(f"✓ Loaded {lang}: {model_name}")
29
+ except Exception as e:
30
+ print(f"✗ Failed to load {lang}: {e}")
31
+
32
+ print(f"Loaded {len(VOICES)} voices: {list(VOICES.keys())}")
33
 
34
  class TTSRequest(BaseModel):
35
  text: str
36
+ lang: str = "zh" # zh, en, ms, ja, ko
37
  speaker_id: int = 0
38
+ sample_rate: int = 22050
 
 
39
 
40
  @app.post("/tts")
41
  async def tts(request: TTSRequest):
42
+ voice = VOICES.get(request.lang)
43
+ if not voice:
44
+ raise HTTPException(400, f"Language '{request.lang}' not available. Available: {list(VOICES.keys())}")
45
 
46
  try:
47
+ audio = voice.synthesize(request.text, speaker_id=request.speaker_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  buffer = BytesIO()
49
+ sf.write(buffer, audio, request.sample_rate, format='WAV')
50
  buffer.seek(0)
 
51
  return Response(
52
  content=buffer.read(),
53
  media_type="audio/wav",
54
+ headers={"Content-Disposition": f"attachment; filename=tts_{uuid.uuid4().hex}.wav"}
 
 
55
  )
56
  except Exception as e:
57
+ raise HTTPException(500, f"Synthesis failed: {str(e)}")
58
 
59
  @app.get("/health")
60
  async def health():
61
+ return {"status": "healthy", "voices": list(VOICES.keys())}
62
 
63
  @app.get("/")
64
  async def root():
65
  return {
66
+ "message": "Piper TTS API - Fast multilingual TTS",
67
+ "voices": list(VOICES.keys()),
68
  "endpoint": "/tts (POST)",
69
+ "example": {"text": "你好,这是 Piper TTS 测试", "lang": "zh"}
 
 
 
 
70
  }
requirements.txt CHANGED
@@ -1,6 +1,5 @@
1
  fastapi==0.111.0
2
  uvicorn[standard]==0.30.1
3
- matcha-tts==0.0.7.2
4
- onnxruntime==1.19.2
5
  soundfile==0.12.1
6
  pydantic==2.8.2
 
1
  fastapi==0.111.0
2
  uvicorn[standard]==0.30.1
3
+ piper-tts==1.2.0
 
4
  soundfile==0.12.1
5
  pydantic==2.8.2