# MOSS-TTS-Nano Space API Examples Base URL: ```txt https://wuxiandashu-tts.hf.space ``` ## Endpoints ### Health Check ```http GET /health ``` Example response: ```json { "ok": true, "runtime_loaded": true, "runtime_error": null } ``` ### Voice List ```http GET /voices ``` Returns built-in voice IDs and Chinese display labels. Use the `id` value when calling `/generate`. ### Streaming ASR ```txt wss://wuxiandashu-tts.hf.space/ws/asr ``` The WebSocket uses protocol version 2 and stays open across utterances. Connect and warm the models once, then reuse the same socket for every recognition: ```txt server -> {"type":"connected","protocol":2} client -> {"type":"warmup"} server -> {"type":"loading"} # only on a cold model server -> {"type":"warmed","loaded":true} client -> {"type":"start","sample_rate":16000,"mode":"dialog","expected":""} server -> {"type":"ready","sample_rate":16000,"mode":"dialog"} client -> binary PCM16 frames server -> {"type":"partial","text":"..."} client -> {"type":"stop"} server -> {"type":"final","text":"...","online_text":"..."} server -> {"type":"stopped"} client -> {"type":"start", ...} # next utterance, same socket ``` Audio frames must contain mono, little-endian signed PCM16. Frames of 50-200 ms work well. Declare the actual sample rate in every `start` command. Valid modes are `dialog` and `pinyin`; `pinyin` also requires `expected` such as `a`, `ma1`, or `la4`. Python persistent-connection example: ```python import asyncio import json import wave import websockets URL = "wss://wuxiandashu-tts.hf.space/ws/asr" async def receive_until(socket, expected_type): while True: message = json.loads(await socket.recv()) if message["type"] == "error": raise RuntimeError(message["message"]) if message["type"] == expected_type: return message async def recognize_wav(socket, filename, *, mode="dialog", expected=""): with wave.open(filename, "rb") as audio: if audio.getnchannels() != 1 or audio.getsampwidth() != 2: raise ValueError("WAV must be mono PCM16") sample_rate = audio.getframerate() await socket.send(json.dumps({ "type": "start", "sample_rate": sample_rate, "mode": mode, "expected": expected, })) await receive_until(socket, "ready") frames_per_chunk = max(1, sample_rate // 10) while chunk := audio.readframes(frames_per_chunk): await socket.send(chunk) await socket.send(json.dumps({"type": "stop"})) final = None while True: message = json.loads(await socket.recv()) if message["type"] == "partial": print("partial:", message["text"]) elif message["type"] == "final": final = message elif message["type"] == "error": raise RuntimeError(message["message"]) elif message["type"] == "stopped": return final async def main(): async with websockets.connect(URL, open_timeout=30) as socket: await receive_until(socket, "connected") await socket.send(json.dumps({"type": "warmup"})) await receive_until(socket, "warmed") first = await recognize_wav(socket, "first.wav") second = await recognize_wav(socket, "second.wav", mode="pinyin", expected="a") print("first:", first["text"]) print("second:", second["pinyin"]) asyncio.run(main()) ``` For live audio, send each microphone frame as it arrives instead of reading a WAV file. Send `{"type":"ping"}` during long application-level idle periods if your WebSocket client does not already send protocol ping frames. ### Fast Mandarin TTS WebSocket Use this endpoint for the lowest CPU latency. It uses the lightweight `sherpa-onnx-vits-zh-ll` model and supports five speaker IDs (`0` through `4`). Keep one connection open and reuse it: ```txt wss://wuxiandashu-tts.hf.space/ws/tts-fast ``` ```txt server -> {"type":"connected","protocol":1,"engine":"sherpa-onnx-vits-zh-ll"} client -> {"type":"warmup"} server -> {"type":"loading"} # cold process only server -> {"type":"warmed","sample_rate":16000,"channels":1,"num_speakers":5,...} client -> {"type":"synthesize","request_id":"r1","text":"你好","speaker":0,"speed":1.0,"audio_format":"pcm_s16le"} server -> {"type":"started","request_id":"r1","sample_rate":16000,"channels":1,...} server -> binary PCM16 chunks server -> {"type":"completed","request_id":"r1","first_audio_ms":...,"elapsed_ms":...} ``` `speaker` defaults to `0`; the five labels are `Suyingxue`, `Gunian`, `Fushiyu`, `Bingjiao`, and `Bazong`. `speed` accepts `0.5` through `2.0`. The native output is already 16 kHz mono, so use `pcm_s16le`. This fast path does not provide voice cloning or audio effects. For a complete WAV response instead of streamed PCM: ```bash curl -L -o fast.wav \ -F "text=你好,我们来讲故事。" \ -F "speaker=0" \ -F "speed=1.0" \ https://wuxiandashu-tts.hf.space/generate-fast ``` ### MOSS Streaming TTS WebSocket For MOSS built-in voices, keep one connection open and reuse it: ```txt wss://wuxiandashu-tts.hf.space/ws/tts ``` ```txt server -> {"type":"connected","protocol":1} client -> {"type":"warmup"} server -> {"type":"loading"} # cold model only server -> {"type":"warmed","sample_rate":48000,"channels":2,"encoding":"pcm_s16le"} client -> {"type":"synthesize","request_id":"r1","text":"...","voice":"Junhao","audio_format":"pcm_s16le_16k_mono"} server -> {"type":"started","request_id":"r1","sample_rate":16000,"channels":1,"encoding":"pcm_s16le"} server -> binary PCM16 chunks server -> {"type":"completed","request_id":"r1","first_audio_ms":...,"elapsed_ms":...} ``` Binary messages are headerless, interleaved, signed PCM16 little-endian. Start playback when the first binary message arrives. Do not wait for `completed`. Send the next `synthesize` command after `completed` and reuse the same socket. Use `pcm_s16le` for native 48 kHz stereo, or `pcm_s16le_16k_mono` to reduce public-network bandwidth by 6x. The compact format is recommended across the public Hugging Face endpoint. The streaming endpoint currently supports built-in voices with `effect=none`. Use `POST /generate` for reference-audio voice cloning or whole-waveform effects. See `SERVICE_GUIDE.md` for a complete persistent Python client, deployment and latency guidance, voice cloning, fine-tuning, and ONNX export instructions. ### Generate Speech ```http POST /generate Content-Type: multipart/form-data ``` Form fields: | Field | Required | Example | Description | |---|---:|---|---| | `text` | yes | `你好,这是测试。` | Text to synthesize. Keep it short on CPU Basic. | | `mode` | no | `builtin` | `builtin` or `clone`. | | `voice` | no | `Junhao` | Built-in voice ID. Used only in `builtin` mode. | | `effect` | no | `none` | Voice effect preset. | | `prompt_audio` | clone only | `voice.wav` | Reference audio file for voice clone mode. | Available `effect` values: ```txt none deep bright monster robot telephone radio fast slow cinema ``` The response is a WAV file: ```http Content-Type: audio/wav ``` Useful response headers: ```txt X-Elapsed-Seconds X-Sample-Rate X-Mode X-Effect ``` ## cURL Built-in voice: ```bash curl -L -o output.wav \ -F "mode=builtin" \ -F "voice=Junhao" \ -F "effect=deep" \ -F "text=你好,这是一个外部服务调用测试。" \ https://wuxiandashu-tts.hf.space/generate ``` Voice clone: ```bash curl -L -o clone.wav \ -F "mode=clone" \ -F "effect=radio" \ -F "text=这是音色克隆接口调用测试。" \ -F "prompt_audio=@prompt.wav;type=audio/wav" \ https://wuxiandashu-tts.hf.space/generate ``` ## Python ```python import requests BASE_URL = "https://wuxiandashu-tts.hf.space" data = { "mode": "builtin", "voice": "Junhao", "effect": "telephone", "text": "你好,这是 Python 调用测试。", } response = requests.post(f"{BASE_URL}/generate", data=data, timeout=300) response.raise_for_status() with open("output.wav", "wb") as f: f.write(response.content) print("elapsed:", response.headers.get("X-Elapsed-Seconds")) print("effect:", response.headers.get("X-Effect")) ``` Voice clone: ```python import requests BASE_URL = "https://wuxiandashu-tts.hf.space" data = { "mode": "clone", "effect": "monster", "text": "这是 Python 音色克隆调用测试。", } with open("prompt.wav", "rb") as audio: files = { "prompt_audio": ("prompt.wav", audio, "audio/wav"), } response = requests.post(f"{BASE_URL}/generate", data=data, files=files, timeout=300) response.raise_for_status() with open("clone.wav", "wb") as f: f.write(response.content) ``` ## Node.js Node.js 18+ has built-in `fetch`, `FormData`, and `Blob`. ```js import { readFile, writeFile } from "node:fs/promises"; const baseUrl = "https://wuxiandashu-tts.hf.space"; const form = new FormData(); form.append("mode", "builtin"); form.append("voice", "Junhao"); form.append("effect", "cinema"); form.append("text", "你好,这是 Node.js 调用测试。"); const response = await fetch(`${baseUrl}/generate`, { method: "POST", body: form, }); if (!response.ok) { throw new Error(await response.text()); } const audio = Buffer.from(await response.arrayBuffer()); await writeFile("output.wav", audio); ``` Voice clone: ```js import { readFile, writeFile } from "node:fs/promises"; const baseUrl = "https://wuxiandashu-tts.hf.space"; const prompt = await readFile("prompt.wav"); const form = new FormData(); form.append("mode", "clone"); form.append("effect", "robot"); form.append("text", "这是 Node.js 音色克隆调用测试。"); form.append("prompt_audio", new Blob([prompt], { type: "audio/wav" }), "prompt.wav"); const response = await fetch(`${baseUrl}/generate`, { method: "POST", body: form, }); if (!response.ok) { throw new Error(await response.text()); } const audio = Buffer.from(await response.arrayBuffer()); await writeFile("clone.wav", audio); ``` ## Go Built-in voice: ```go package main import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "os" ) func addField(w *multipart.Writer, key, value string) { _ = w.WriteField(key, value) } func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) addField(writer, "mode", "builtin") addField(writer, "voice", "Junhao") addField(writer, "effect", "deep") addField(writer, "text", "你好,这是 Go 调用测试。") _ = writer.Close() req, err := http.NewRequest("POST", "https://wuxiandashu-tts.hf.space/generate", &body) if err != nil { panic(err) } req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) panic(fmt.Sprintf("request failed: %s %s", resp.Status, string(b))) } out, err := os.Create("output.wav") if err != nil { panic(err) } defer out.Close() _, _ = io.Copy(out, resp.Body) fmt.Println("elapsed:", resp.Header.Get("X-Elapsed-Seconds")) fmt.Println("effect:", resp.Header.Get("X-Effect")) } ``` Voice clone: ```go package main import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "os" ) func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) _ = writer.WriteField("mode", "clone") _ = writer.WriteField("effect", "robot") _ = writer.WriteField("text", "这是 Go 音色克隆调用测试。") fileWriter, err := writer.CreateFormFile("prompt_audio", "prompt.wav") if err != nil { panic(err) } file, err := os.Open("prompt.wav") if err != nil { panic(err) } _, _ = io.Copy(fileWriter, file) _ = file.Close() _ = writer.Close() req, err := http.NewRequest("POST", "https://wuxiandashu-tts.hf.space/generate", &body) if err != nil { panic(err) } req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) panic(fmt.Sprintf("request failed: %s %s", resp.Status, string(b))) } out, err := os.Create("clone.wav") if err != nil { panic(err) } defer out.Close() _, _ = io.Copy(out, resp.Body) } ``` ## Notes - The Space runs on free CPU Basic, so use a long timeout such as 300 seconds. - Keep `text` short for stable latency. - Public Spaces can sleep after inactivity; the first request after sleep may be slow. - Voice clone works best with a clear 5-15 second single-speaker reference audio file.