Jacky2305 commited on
Commit
4e65862
·
1 Parent(s): 98ec627

Piper TTS: runtime model download, no build-time network

Browse files
Files changed (3) hide show
  1. Dockerfile +1 -15
  2. main.py +65 -19
  3. requirements.txt +1 -0
Dockerfile CHANGED
@@ -6,26 +6,12 @@ WORKDIR /app
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
 
@@ -33,7 +19,7 @@ COPY main.py .
33
  EXPOSE 7860
34
 
35
  # Health check
36
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
37
  CMD curl -f http://localhost:7860/health || exit 1
38
 
39
  # Run the application
 
6
  RUN apt-get update && apt-get install -y \
7
  libsndfile1 \
8
  espeak-ng \
 
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
 
 
19
  EXPOSE 7860
20
 
21
  # Health check
22
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
23
  CMD curl -f http://localhost:7860/health || exit 1
24
 
25
  # Run the application
main.py CHANGED
@@ -6,30 +6,75 @@ 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
@@ -39,9 +84,10 @@ class TTSRequest(BaseModel):
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)
@@ -58,13 +104,13 @@ async def tts(request: TTSRequest):
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
  }
 
6
  import uuid
7
  from fastapi.responses import Response
8
  from io import BytesIO
9
+ import requests
10
+ from pathlib import Path
11
 
12
  app = FastAPI(title="Piper TTS Space")
13
 
14
+ # Voice model info: (lang, model_name, url_onnx, url_json)
15
  VOICE_MODELS = {
16
+ "zh": ("zh_CN-huayan-medium",
17
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx",
18
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx.json"),
19
+ "en": ("en_US-lessac-medium",
20
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx",
21
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"),
22
+ "ms": ("ms_MY-sirius-medium",
23
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx",
24
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx.json"),
25
+ "ja": ("ja_JP-kenji-medium",
26
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx",
27
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx.json"),
28
+ "ko": ("ko_KR-hui-hao-medium",
29
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx",
30
+ "https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx.json"),
31
  }
32
 
33
+ MODEL_DIR = Path("/tmp/piper_models")
34
+ MODEL_DIR.mkdir(exist_ok=True)
35
  VOICES = {}
36
 
37
+ def download_file(url: str, dest: Path):
38
+ """Download file with progress, follow redirects."""
39
+ with requests.get(url, stream=True, timeout=60, allow_redirects=True) as r:
40
+ r.raise_for_status()
41
+ with open(dest, 'wb') as f:
42
+ for chunk in r.iter_content(chunk_size=8192):
43
+ f.write(chunk)
44
+
45
+ def get_voice(lang: str) -> PiperVoice:
46
+ """Load voice, downloading model if needed."""
47
+ if lang in VOICES:
48
+ return VOICES[lang]
49
+
50
+ if lang not in VOICE_MODELS:
51
+ raise ValueError(f"Language '{lang}' not available. Available: {list(VOICE_MODELS.keys())}")
52
+
53
+ model_name, url_onnx, url_json = VOICE_MODELS[lang]
54
+ onnx_path = MODEL_DIR / f"{model_name}.onnx"
55
+ json_path = MODEL_DIR / f"{model_name}.onnx.json"
56
+
57
+ # Download if not cached
58
+ if not onnx_path.exists():
59
+ print(f"Downloading {model_name}...")
60
+ download_file(url_onnx, onnx_path)
61
+ if not json_path.exists():
62
+ print(f"Downloading {model_name}.json...")
63
+ download_file(url_json, json_path)
64
+
65
+ voice = PiperVoice.load(str(onnx_path))
66
+ VOICES[lang] = voice
67
+ print(f"✓ Loaded {lang}: {model_name}")
68
+ return voice
69
+
70
+ # Pre-load default voice (zh) on startup to warm up
71
+ @app.on_event("startup")
72
+ async def warmup():
73
  try:
74
+ get_voice("zh")
75
+ print("Warmup complete")
 
76
  except Exception as e:
77
+ print(f"Warmup failed (will retry on first request): {e}")
 
 
78
 
79
  class TTSRequest(BaseModel):
80
  text: str
 
84
 
85
  @app.post("/tts")
86
  async def tts(request: TTSRequest):
87
+ try:
88
+ voice = get_voice(request.lang)
89
+ except Exception as e:
90
+ raise HTTPException(400, f"Voice load failed: {str(e)}")
91
 
92
  try:
93
  audio = voice.synthesize(request.text, speaker_id=request.speaker_id)
 
104
 
105
  @app.get("/health")
106
  async def health():
107
+ return {"status": "healthy", "loaded_voices": list(VOICES.keys()), "available_voices": list(VOICE_MODELS.keys())}
108
 
109
  @app.get("/")
110
  async def root():
111
  return {
112
  "message": "Piper TTS API - Fast multilingual TTS",
113
+ "available_voices": list(VOICE_MODELS.keys()),
114
  "endpoint": "/tts (POST)",
115
  "example": {"text": "你好,这是 Piper TTS 测试", "lang": "zh"}
116
  }
requirements.txt CHANGED
@@ -3,3 +3,4 @@ uvicorn[standard]==0.30.1
3
  piper-tts==1.2.0
4
  soundfile==0.12.1
5
  pydantic==2.8.2
 
 
3
  piper-tts==1.2.0
4
  soundfile==0.12.1
5
  pydantic==2.8.2
6
+ requests==2.32.3