Yue Phone API
Cantonese speech to Jyutping phonemes (wav2vec2-BERT). Base URL: https://yue-phone.hon9kon9ize.com
Authentication
Every /v1 request needs an API key in the Authorization header:
Authorization: Bearer yp_xxxxxxxxxxxxxxxxxxxxxxxx
Keys are issued by the service administrator and shown once at creation. Keep yours secret and send it only from server-side code. A missing, revoked or unknown key, or a disabled user, returns 401.
POST/v1/phoneme
Upload one audio clip and get the recognised Jyutping phonemes.
| Request | multipart/form-data with one field file: a 16 kHz mono .wav is best (other rates are resampled; stereo is mixed down). Max 10 MB. |
|---|---|
| Response | 200 JSON, passed through from the model service. |
curl https://yue-phone.hon9kon9ize.com/v1/phoneme \
-H "Authorization: Bearer $YUE_PHONE_KEY" \
-F "file=@clip.wav"
Example response:
{
"phonemes": "ng aa5 | g o2 | j au5 | ...",
"audio_seconds": 2.44,
"infer_seconds": 1.6,
"rtf": 0.656
}
Phonemes are onset and rime+tone tokens, for example m aa4 is maa4. | separates syllables. Look up the IPA of each token with GET /v1/ipa.
Python
import requests
r = requests.post(
"https://yue-phone.hon9kon9ize.com/v1/phoneme",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": open("clip.wav", "rb")},
timeout=90,
)
r.raise_for_status()
print(r.json()["phonemes"])
JavaScript
const form = new FormData();
form.set("file", new Blob([wavBytes], { type: "audio/wav" }), "clip.wav");
const res = await fetch("https://yue-phone.hon9kon9ize.com/v1/phoneme", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
});
const { phonemes } = await res.json();
POST/v1/phoneme/detail
Same request as /v1/phoneme. The response also lists each syllable the model heard: toned syllable, onset, rime, tone, IPA, time box in seconds, confidence, alternatives and token posteriors. Confidence and boxes are hints, not error probabilities or a forced alignment.
GET/v1/ipa
Table mapping Jyutping onsets and rimes to IPA.
Rate limits
Limits are per user, as a token bucket. The default is 5 requests per second; your administrator can set a different value for your account. Every /v1 response carries:
X-RateLimit-Limit | your requests per second |
X-RateLimit-Remaining | requests left in the current burst |
Retry-After | on 429 only: seconds to wait |
The bucket holds one second's worth of requests and refills continuously. With the default, a burst of 5 goes through at once, then about one request every 200 ms. The limit is per user, so all keys of one user share it.
When you exceed it, the request is rejected immediately with 429. It is not sent to the model and not stored, so retrying is safe:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
{"error": "rate limit exceeded", "limit_per_sec": 5}
Retry-After is whole seconds, rounded up, minimum 1. Retry example:
import time, requests
def transcribe(path, retries=3):
for _ in range(retries + 1):
r = requests.post(URL, headers=HEADERS, files={"file": open(path, "rb")}, timeout=90)
if r.status_code != 429:
r.raise_for_status()
return r.json()
time.sleep(float(r.headers.get("Retry-After", 1)))
raise RuntimeError("rate limited")
Errors
| Status | Meaning |
|---|---|
400 | No file field, or the file is empty |
401 | Missing or invalid API key |
413 | File larger than 10 MB |
429 | Rate limit exceeded; wait Retry-After seconds |
502 / 504 | The model service failed or timed out. The body includes request_id; quote it when reporting problems |
Error bodies look like {"error": "...", "request_id": "..."}. Successful responses carry the same id in the X-Request-Id header.
Data retention
Uploaded audio and the returned results are stored with your request record for quality review and debugging.