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.

Requestmultipart/form-data with one field file: a 16 kHz mono .wav is best (other rates are resampled; stereo is mixed down). Max 10 MB.
Response200 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-Limityour requests per second
X-RateLimit-Remainingrequests left in the current burst
Retry-Afteron 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

StatusMeaning
400No file field, or the file is empty
401Missing or invalid API key
413File larger than 10 MB
429Rate limit exceeded; wait Retry-After seconds
502 / 504The 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.