Version 0.2 adds lint() (check a reference text before recording, free), status() (the synthetic engine probe), the compact response option, test keys, and a toneperfect-lint command line for whole lesson files. Complete apps that use the API through a small server proxy are on the starter projects page.
These clients are not on PyPI or npm yet. Until they are, download the file below — it is the whole client — and keep it in your repository. Package releases will be announced on the blog.
Python
One module, standard library only, Python 3.9+. Files: toneperfect.py · README · example script · MIT licence.
curl -O https://api.toneperfect.app/sdk/python/toneperfect.py # drop it next to your codefrom toneperfect import TonePerfect
client = TonePerfect(api_key="tp_live_...") # or set TONEPERFECT_API_KEY
result = client.assess("hello.wav", text="你好", language="zh-CN")
print(result.assessable, result.scores["overall"], result.billing)
for word in result.words:
print(word["word"], word["score"], [p for p in word["phones"] if p["decision"] == "review"])assess() returns an Assessment: .data is the untouched JSON body; .assessable, .reason, .scores, .words, .billing, .model_version, .id and .replayed mirror its documented fields. Every non-2xx status raises TonePerfectError with the API's own error.code, plus retryable, needs_new_key, finalized and, for a finalized failure, its id and billing.
Node.js
Plain JavaScript, ESM and CommonJS, no build step, Node 18+ (global fetch). Files: index.cjs · index.mjs (ESM wrapper) · index.d.ts (types) · package.json · README · example script · MIT licence.
mkdir -p vendor/toneperfect && cd vendor/toneperfect
curl -O https://api.toneperfect.app/sdk/node/index.cjs -O https://api.toneperfect.app/sdk/node/index.mjs \
-O https://api.toneperfect.app/sdk/node/index.d.ts -O https://api.toneperfect.app/sdk/node/package.jsonimport { TonePerfect } from './vendor/toneperfect/index.mjs'; // or require('./vendor/toneperfect/index.cjs')
const client = new TonePerfect({ apiKey: 'tp_live_...' }); // or set TONEPERFECT_API_KEY
const result = await client.assess('hello.wav', { text: '你好', language: 'zh-CN' });
console.log(result.assessable, result.scores.overall, result.billing);
for (const word of result.words) {
console.log(word.word, word.score, word.phones.filter(p => p.decision === 'review'));
}audio is a file path, Buffer, ArrayBuffer or Blob. Errors reject with TonePerfectError (status, code, detail, requestId, retryable, needsNewKey, finalized, id, billing).
What both clients do
| Method | Endpoint | Notes |
|---|---|---|
capabilities() | GET /v1/capabilities | No key needed. Languages with status (all experimental today), dimensions, limits, model_version. |
assess(audio, text, language, pinyin, task, idempotency key) | POST /v1/assess | Multipart audio, text, language, optional pinyin (numbered tones) and task (general, connected_speech, isolated_word). A key is generated per attempt when you do not pass one. |
Accounts, API keys, usage and billing are managed in the dashboard and are deliberately not wrapped: those routes are session-based. Keep your key on your server; browser and mobile apps should call your own backend.
The decision on every word and sound is one of review, uncertain or no_correction; unassessable (no evidence for that unit), partial (a word only partly covered by the phonetic inventory) and legacy_correction (Mandarin components decided by the legacy policy) also occur. Treat any other value as "no decision".
Safe retries
Every attempt carries a unique Idempotency-Key (generated when you do not pass one). If a network error prevents you from receiving a result, retry with the same key and identical audio, reference text, language, pinyin and practice type: a completed request is returned without charging again, including a completed failure. Use a new key for a different attempt or to try again after a definite failed response. Reusing a key with a different request returns a conflict. Keep the same key when the result is unknown or still pending, including an HTML, empty or unrecognized 502/504 proxy response — an HTTP status alone does not prove that the assessment stopped. A finalized backend failure includes a req_… result id, billing.credits: 0 and a completed error code such as engine_unavailable. Do not generate a new key inside an automatic retry loop. Stored results expire after 24 hours; an expired key returns a conflict and never causes another charge.
The clients decide this from the response body, not the status: retryable means nothing was finalized and the same key should be retried later (rate_limit, account_busy, gateway_busy, decoder_busy, audio_decoder_unavailable, upload_timeout, request_pending, transport timeouts and non-JSON 502/504); needs_new_key / needsNewKey means the failure was stored under the key (finalized: id and billing present) or the key has expired, and a new attempt needs a new key.
What the numbers mean
Grades are 0–100 display scores calibrated per unit against native-speaker references; they are not accuracy percentages and not comparable across vendors. Correction decisions are separate from grades: show the grade, act on the decision. Every language pack is currently labelled experimental. The full account is on the methodology page.
Other languages
The API is one multipart POST; any HTTP client works. The documentation has a cURL example, and the two clients above are short enough to port in an afternoon. Want a client for another language listed here? Tell us.