binhqd commited on
Commit
b840c7e
·
1 Parent(s): c825e17

Add custom inference handler for Maya1 TTS

Browse files
Files changed (3) hide show
  1. README.md +119 -0
  2. handler.py +210 -0
  3. requirements.txt +5 -0
README.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # text-to-speech-maya
2
+
3
+ A small playground for the Maya text-to-speech inference handler and a FastAPI test server.
4
+
5
+ ## Run the FastAPI test server (local)
6
+
7
+ 1. Install dependencies. Follow the official PyTorch instructions for your platform first (CPU/MPS/CUDA), then install the remaining requirements:
8
+
9
+ ```bash
10
+ # Example: install torch from https://pytorch.org/ for your system first, then:
11
+ pip install -r inference_endpoints/requirements.txt
12
+ ```
13
+
14
+ 2. Start the server (set `MAYA_MODEL_PATH` to your model path if needed):
15
+
16
+ ```bash
17
+ export MAYA_MODEL_PATH="/path/to/maya_model"
18
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
19
+ ```
20
+
21
+ 3. Test the synthesis endpoint (saves `output.wav`):
22
+
23
+ ```bash
24
+ curl -X POST "http://localhost:8000/synthesize" -H "Content-Type: application/json" -d '{
25
+ "description": "neutral female voice",
26
+ "text": "Hello world from Maya TTS"
27
+ }' --output output.wav
28
+ ```
29
+
30
+ Notes
31
+
32
+ - The `EndpointHandler` expects the Maya model to be compatible with the SNAC decoder flow; you may need to adapt token decoding depending on the actual model outputs.
33
+ - `torch` often requires platform-specific wheels; if `pip install -r` fails for `torch`, install the appropriate wheel from pytorch.org first then re-run the requirements install for the remaining packages.
34
+
35
+ ---
36
+
37
+ ## VSCode setup (recommended)
38
+
39
+ The repo includes helper files to make development in VSCode convenient: a workspace `.venv`, editor settings, tasks, and launch configurations.
40
+
41
+ 1. Create and activate a workspace venv (the Makefile has a helper):
42
+
43
+ ```bash
44
+ make venv
45
+ source .venv/bin/activate
46
+ ```
47
+
48
+ 2. Install project requirements into the venv:
49
+
50
+ ```bash
51
+ pip install -r inference_endpoints/requirements.txt
52
+ # dev tools for formatting & linting
53
+ pip install black flake8
54
+ ```
55
+
56
+ 3. Open this folder in VSCode. The workspace settings point to `${workspaceFolder}/.venv/bin/python` and the Python extension will activate the venv in the terminal.
57
+
58
+ 4. Useful VSCode features included:
59
+
60
+ - Formatting: `Black` is configured and will run on save (line length 88).
61
+ - Linting: `flake8` is enabled; run it using the Tasks panel or via the command palette.
62
+ - Tasks: Run `Format: black`, `Lint: flake8`, or `Run server (run.sh)` from the Tasks menu.
63
+ - Launch: Use the `Run FastAPI (uvicorn)` launch configuration to start the server with debugging enabled. It will load `server/.env` for environment variables.
64
+
65
+ 5. Running the server from VSCode:
66
+
67
+ - Use the Run panel and choose `Run FastAPI (uvicorn)` to start with the debugger.
68
+ - Or open the integrated terminal, ensure the venv is active, and run:
69
+
70
+ ```bash
71
+ ./run.sh
72
+ ```
73
+
74
+ 6. Poetry users
75
+
76
+ If you prefer Poetry for the server package, there's a `server/pyproject.toml`. From `server/` run:
77
+
78
+ ```bash
79
+ poetry install
80
+ poetry run uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
81
+ ```
82
+
83
+ ---
84
+
85
+ If you want I can add a short checklist or developer guide in `DEVELOPING.md` with these steps and troubleshooting tips.
86
+
87
+ # text-to-speech-maya
88
+
89
+ A small playground for the Maya text-to-speech inference handler and a FastAPI test server.
90
+
91
+ ## Run the FastAPI test server (local)
92
+
93
+ 1. Install dependencies. Follow the official PyTorch instructions for your platform first (CPU/MPS/CUDA), then install the remaining requirements:
94
+
95
+ ```bash
96
+ # Example: install torch from https://pytorch.org/ for your system first, then:
97
+ pip install -r inference_endpoints/requirements.txt
98
+ ```
99
+
100
+ 2. Start the server (set `MAYA_MODEL_PATH` to your model path if needed):
101
+
102
+ ```bash
103
+ export MAYA_MODEL_PATH="/path/to/maya_model"
104
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
105
+ ```
106
+
107
+ 3. Test the synthesis endpoint (saves `output.wav`):
108
+
109
+ ```bash
110
+ curl -X POST "http://localhost:8000/synthesize" -H "Content-Type: application/json" -d '{
111
+ "description": "neutral female voice",
112
+ "text": "Hello world from Maya TTS"
113
+ }' --output output.wav
114
+ ```
115
+
116
+ Notes
117
+
118
+ - The `EndpointHandler` expects the Maya model to be compatible with the SNAC decoder flow; you may need to adapt token decoding depending on the actual model outputs.
119
+ - `torch` often requires platform-specific wheels; if `pip install -r` fails for `torch`, install the appropriate wheel from pytorch.org first then re-run the requirements install for the remaining packages.
handler.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import os
4
+ import struct
5
+ import wave
6
+
7
+
8
+ class EndpointHandler:
9
+ def __init__(self, path=""):
10
+ # allow overriding device and dtype via environment variables for local CPU testing
11
+ # `MAYA_DEVICE`: 'cpu' or 'auto' (default 'auto')
12
+ # `MAYA_TORCH_DTYPE`: 'bf16' or 'fp32' (default 'bf16')
13
+ # `MAYA_USE_FAKE`: if '1', use a tiny fake pipeline for smoke testing (no HF downloads)
14
+ device_override = os.getenv("MAYA_DEVICE", "auto")
15
+ dtype_override = os.getenv("MAYA_TORCH_DTYPE", "bf16")
16
+ use_fake = os.getenv("MAYA_USE_FAKE", "0") == "1"
17
+
18
+ # map string to torch dtype will be set after importing torch
19
+
20
+ if use_fake:
21
+ # Minimal fake components for quick local smoke tests. Generates a short sine tone.
22
+ self.model = None
23
+ self.tokenizer = None
24
+ # keep device as plain string for fake mode
25
+ self.device = "cpu"
26
+ self.snac = None
27
+ # no external libs required for fake mode
28
+ self.sf = None
29
+ return
30
+
31
+ # import heavy inference dependencies lazily so fake-mode doesn't require them
32
+ try:
33
+ import soundfile as sf
34
+ import torch
35
+ from snac import SNAC
36
+ from transformers import AutoModelForCausalLM, AutoTokenizer
37
+ except Exception as e:
38
+ raise RuntimeError(
39
+ f"Failed to import inference dependencies: {e}.\n"
40
+ "Install the packages listed in `inference_endpoints/requirements.txt`."
41
+ )
42
+
43
+ # map string to torch dtype
44
+ torch_dtype = torch.bfloat16 if dtype_override == "bf16" else torch.float32
45
+
46
+ # load Maya1 text-to-voice model
47
+ # force CPU device_map when requested to avoid trying to use GPUs
48
+ device_map_arg = "auto"
49
+ if device_override == "cpu":
50
+ device_map_arg = "cpu"
51
+
52
+ self.model = AutoModelForCausalLM.from_pretrained(
53
+ path, torch_dtype=torch_dtype, device_map=device_map_arg
54
+ )
55
+ self.tokenizer = AutoTokenizer.from_pretrained(path)
56
+
57
+ # determine device from model parameters (safer than using `model.device`)
58
+ try:
59
+ self.device = next(self.model.parameters()).device
60
+ except StopIteration:
61
+ # fallback to CPU if model has no parameters
62
+ self.device = torch.device("cpu")
63
+
64
+ # load SNAC model (audio decoder) 24 kHz
65
+ self.snac = (
66
+ SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval().to(self.device)
67
+ )
68
+ self.sf = sf
69
+
70
+ def __call__(self, data):
71
+ """
72
+ Expect `data` dict like:
73
+ {
74
+ "description": "... voice description ...",
75
+ "text": "... text to speak ...",
76
+ "generation_args": { optional dict for text generation params }
77
+ }
78
+ Returns dict with base64 audio:
79
+ {
80
+ "audio_base64": "<base64-encoded WAV data>",
81
+ "sampling_rate": 24000
82
+ }
83
+ """
84
+ description = data.get("description", "")
85
+ text = data.get("text", "")
86
+ if not text:
87
+ return {"error": "No text provided."}
88
+ prompt = description + "\n" + text
89
+
90
+ # If running in fake mode (quick smoke test), synthesize a sine tone
91
+ if getattr(self, "snac", None) is None and getattr(self, "model", None) is None:
92
+ # generate a 1-second 24kHz sine wave and write a 16-bit WAV using stdlib
93
+ sr = 24000
94
+ duration = 1.0
95
+ n_samples = int(sr * duration)
96
+ freq = 220.0
97
+ # generate samples without numpy
98
+ waveform = [
99
+ int(
100
+ 0.1
101
+ * 32767
102
+ * __import__("math").sin(
103
+ 2 * __import__("math").pi * freq * (i / sr)
104
+ )
105
+ )
106
+ for i in range(n_samples)
107
+ ]
108
+
109
+ buf = io.BytesIO()
110
+ with wave.open(buf, "wb") as wf:
111
+ wf.setnchannels(1)
112
+ wf.setsampwidth(2) # 16-bit
113
+ wf.setframerate(sr)
114
+ # pack samples as little-endian signed 16-bit
115
+ frames = struct.pack("<" + ("h" * len(waveform)), *waveform)
116
+ wf.writeframes(frames)
117
+ wav_bytes = buf.getvalue()
118
+ b64 = base64.b64encode(wav_bytes).decode("utf-8")
119
+
120
+ return {"audio_base64": b64, "sampling_rate": sr}
121
+
122
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
123
+ # generate token ids with default or custom params
124
+ gen_args = data.get("generation_args", {})
125
+ outputs = self.model.generate(**inputs, **gen_args)
126
+ token_ids = outputs[0]
127
+
128
+ # decode tokens to intermediate representation (for Maya1)
129
+ # assuming model outputs token ids for audio generation — adjust as per model spec
130
+ audio_feats = (
131
+ token_ids # may need further decoding depending on how Maya1 works
132
+ )
133
+
134
+ # pass features to SNAC to synthesize waveform
135
+ waveform = self.snac.decode(audio_feats).cpu().numpy() # shape (n_samples,)
136
+
137
+ # convert waveform to bytes (e.g. WAV) using soundfile loaded into self.sf
138
+ buf = io.BytesIO()
139
+ self.sf.write(buf, waveform, 24000, format="WAV")
140
+ wav_bytes = buf.getvalue()
141
+ b64 = base64.b64encode(wav_bytes).decode("utf-8")
142
+
143
+ return {
144
+ "audio_base64": b64,
145
+ "sampling_rate": 24000,
146
+ }
147
+
148
+
149
+ # Module-level convenience functions for hosting platforms (Hugging Face Endpoints)
150
+ # The platform typically expects top-level `init` and `predict` (or `run`) callables
151
+ # so we provide thin wrappers around the EndpointHandler class.
152
+ _HANDLER = None
153
+
154
+
155
+ def init(model_id: str = None):
156
+ """Initialize the module-level handler.
157
+
158
+ model_id: optional path or model identifier to pass to EndpointHandler.
159
+ If omitted the handler will look for environment variables `HF_MODEL_ID` or
160
+ `MODEL_ID` and otherwise instantiate with a blank path (which may be
161
+ appropriate when the model files are bundled with the repo).
162
+ """
163
+ global _HANDLER
164
+ if _HANDLER is not None:
165
+ return
166
+
167
+ model_path = model_id or os.getenv("HF_MODEL_ID") or os.getenv("MODEL_ID") or ""
168
+ _HANDLER = EndpointHandler(path=model_path)
169
+
170
+
171
+ def predict(payload):
172
+ """Predict/predict wrapper for hosted endpoints.
173
+
174
+ Accepts a dict (recommended) or a JSON-ish payload. If a list is passed,
175
+ the first element is used. If a plain string is provided it is treated as
176
+ the `text` field.
177
+ Returns the same dict structure produced by EndpointHandler.__call__.
178
+ """
179
+ global _HANDLER
180
+ if _HANDLER is None:
181
+ init()
182
+
183
+ data = payload
184
+ # handle list payloads (common in some platform wrappers)
185
+ if isinstance(payload, (list, tuple)) and len(payload) > 0:
186
+ data = payload[0]
187
+
188
+ # allow raw JSON string -> dict
189
+ if isinstance(data, str):
190
+ try:
191
+ import json
192
+
193
+ data = json.loads(data)
194
+ except Exception:
195
+ # treat plain string as the text to synthesize
196
+ data = {"text": data}
197
+
198
+ if not isinstance(data, dict):
199
+ # best-effort normalization
200
+ data = {"text": str(data)}
201
+
202
+ try:
203
+ return _HANDLER(data)
204
+ except Exception as e:
205
+ return {"error": str(e)}
206
+
207
+
208
+ def run(payload):
209
+ """Alias for predict for platforms that expect `run` entrypoint."""
210
+ return predict(payload)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.0
2
+ transformers>=4.30
3
+ snac
4
+ soundfile
5
+ accelerate