Sammydynamo commited on
Commit
c6fda30
·
1 Parent(s): c9dcdd6

Initial TTS microservice deployment

Browse files
Files changed (4) hide show
  1. Dockerfile +36 -0
  2. README.md +11 -8
  3. app.py +121 -0
  4. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install CPU-only torch first (saves ~1.8GB vs CUDA build)
6
+ RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY app.py .
12
+
13
+ # Pre-download commonly used TTS models so first requests are fast.
14
+ # Individual failures are non-fatal.
15
+ RUN python -c "\
16
+ from transformers import VitsModel, AutoTokenizer; \
17
+ models = [ \
18
+ 'facebook/mms-tts-yor', 'facebook/mms-tts-swh', 'facebook/mms-tts-hau', \
19
+ 'facebook/mms-tts-pcm', 'facebook/mms-tts-aka', 'facebook/mms-tts-lug', \
20
+ 'facebook/mms-tts-amh', 'facebook/mms-tts-som', 'facebook/mms-tts-sna', \
21
+ 'khof312/mms-tts-lin', 'facebook/mms-tts-ara', \
22
+ ]; \
23
+ ok = 0; \
24
+ for m in models: \
25
+ try: \
26
+ VitsModel.from_pretrained(m); AutoTokenizer.from_pretrained(m); \
27
+ print(f' OK {m}'); ok += 1 \
28
+ except Exception as e: \
29
+ print(f' SKIP {m}: {e}') \
30
+ ; \
31
+ print(f'{ok}/{len(models)} models cached') \
32
+ "
33
+
34
+ EXPOSE 7860
35
+
36
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,15 @@
1
  ---
2
- title: Afrolingo
3
- emoji:
4
- colorFrom: indigo
5
- colorTo: yellow
6
  sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: TTS microservice for Afrolingo — MMS-TTS VITS inference
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: Afrolingo TTS
3
+ emoji: 🗣️
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
 
 
8
  ---
9
 
10
+ # Afrolingo TTS Service
11
+
12
+ Lightweight TTS microservice hosting MMS-TTS VITS models for African languages.
13
+
14
+ Called by the main Afrolingo backend via HTTP. Keeps torch/transformers
15
+ out of the memory-constrained Render gateway.
app.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Afrolingo TTS microservice — HuggingFace Spaces deployment.
2
+
3
+ Hosts MMS-TTS VITS models in-memory and exposes a ``/synthesize``
4
+ endpoint. Designed to run on HuggingFace Spaces free tier (2 vCPU,
5
+ 16 GB RAM) where there is ample headroom for multiple loaded models.
6
+
7
+ The main Afrolingo backend on Render (512 MB) calls this service via
8
+ HTTP, keeping torch/transformers out of the gateway's memory budget.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ import logging
15
+ import os
16
+ from collections import OrderedDict
17
+ from threading import Lock
18
+
19
+ import numpy as np
20
+ import torch
21
+ from fastapi import FastAPI, HTTPException
22
+ from fastapi.responses import Response
23
+ from pydantic import BaseModel
24
+ from scipy.io import wavfile
25
+ from transformers import AutoTokenizer, VitsModel
26
+
27
+ logger = logging.getLogger("tts-service")
28
+ logging.basicConfig(level=logging.INFO)
29
+
30
+ app = FastAPI(title="Afrolingo TTS Service", version="0.1.0")
31
+
32
+ MAX_MODELS = int(os.getenv("MAX_MODELS", "5"))
33
+
34
+
35
+ # ------------------------------------------------------------------
36
+ # Thread-safe LRU model cache
37
+ # ------------------------------------------------------------------
38
+
39
+ class _ModelCache:
40
+ """Keep up to *max_models* ``(VitsModel, AutoTokenizer)`` pairs in memory."""
41
+
42
+ def __init__(self, max_models: int) -> None:
43
+ self._max = max_models
44
+ self._cache: OrderedDict[str, tuple[VitsModel, AutoTokenizer]] = OrderedDict()
45
+ self._lock = Lock()
46
+
47
+ def get_or_load(self, checkpoint: str) -> tuple[VitsModel, AutoTokenizer]:
48
+ with self._lock:
49
+ if checkpoint in self._cache:
50
+ self._cache.move_to_end(checkpoint)
51
+ return self._cache[checkpoint]
52
+
53
+ # Load outside lock (slow I/O)
54
+ logger.info("Loading model %s ...", checkpoint)
55
+ model = VitsModel.from_pretrained(checkpoint)
56
+ tokenizer = AutoTokenizer.from_pretrained(checkpoint)
57
+ model.eval()
58
+ logger.info("Loaded model %s", checkpoint)
59
+
60
+ with self._lock:
61
+ # Double-check after re-acquiring lock
62
+ if checkpoint in self._cache:
63
+ self._cache.move_to_end(checkpoint)
64
+ return self._cache[checkpoint]
65
+ if len(self._cache) >= self._max:
66
+ evicted_key, _ = self._cache.popitem(last=False)
67
+ logger.info("Evicted model %s", evicted_key)
68
+ self._cache[checkpoint] = (model, tokenizer)
69
+ return (model, tokenizer)
70
+
71
+ @property
72
+ def size(self) -> int:
73
+ return len(self._cache)
74
+
75
+
76
+ _cache = _ModelCache(MAX_MODELS)
77
+
78
+
79
+ # ------------------------------------------------------------------
80
+ # Routes
81
+ # ------------------------------------------------------------------
82
+
83
+ class SynthesizeRequest(BaseModel):
84
+ text: str
85
+ checkpoint: str
86
+
87
+
88
+ @app.post("/synthesize")
89
+ async def synthesize(req: SynthesizeRequest) -> Response:
90
+ """Synthesize speech and return WAV audio bytes."""
91
+ # Load / retrieve model
92
+ try:
93
+ model, tokenizer = _cache.get_or_load(req.checkpoint)
94
+ except Exception as exc:
95
+ logger.error("Model load failed for %s: %s", req.checkpoint, exc)
96
+ raise HTTPException(status_code=503, detail=f"Model loading failed: {exc}")
97
+
98
+ # Inference
99
+ try:
100
+ inputs = tokenizer(req.text, return_tensors="pt")
101
+ with torch.no_grad():
102
+ output = model(**inputs)
103
+
104
+ waveform = output.waveform[0].cpu().numpy()
105
+ sample_rate: int = model.config.sampling_rate
106
+
107
+ waveform = np.clip(waveform, -1.0, 1.0)
108
+ waveform_int16 = (waveform * 32767).astype(np.int16)
109
+
110
+ buf = io.BytesIO()
111
+ wavfile.write(buf, sample_rate, waveform_int16)
112
+ return Response(content=buf.getvalue(), media_type="audio/wav")
113
+
114
+ except Exception as exc:
115
+ logger.error("Inference failed for %s: %s", req.checkpoint, exc)
116
+ raise HTTPException(status_code=500, detail=f"Inference failed: {exc}")
117
+
118
+
119
+ @app.get("/health")
120
+ async def health() -> dict:
121
+ return {"status": "healthy", "cached_models": _cache.size}
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ torch
4
+ transformers
5
+ scipy
6
+ numpy