Per-tone results

Tone is scored separately from the initial and the final of each syllable. In words[].phones[] the row with "dimension": "tone" holds expected and heard as tone numbers (1–4, 5 for neutral), a grade and a decision. The same syllable in syllables[].components.tone adds agrees_with_prompt, a confidence label and alternatives: every tone the engine considered, with its probability.

FieldPer syllable, for the initial, the final and the tone
expectedWhat the reference requires: an initial such as sh, a final such as ang, or a tone number 1–5 (5 = neutral).
heardThe engine’s estimate of what was said, in the same units.
score0–100 display grade against native-speaker references.
decisionreview, uncertain or no_correction: whether your app should correct this component.
agrees_with_prompt, confidence, alternativesIn syllables[].components: whether heard equals expected, a confidence label, and the ranked candidates with probabilities.
start_ms, end_msThe syllable’s position in the recording, for replaying exactly that slice.

A wrong tone, scored

The reference text was 我想卖书 (“I want to sell books”, mài, 4th tone); the recording said 我想买书 (“buy books”, mǎi, 3rd tone). The real response for that syllable, trimmed to the documented fields:

JSON · 200 OK (real response, trimmed)
{
  "id": "req_example",
  "language": "zh",
  "locale": "zh-CN",
  "model_version": "mandarin-human-v2-85741cdf3967-fbf73d85ee88",
  "task": "connected_speech",
  "status": "completed",
  "assessable": true,
  "reason": null,
  "scores": {
    "overall": 99.3,
    "pronunciation": 97.8,
    "initial": 97.8,
    "final": 97.9,
    "tone": 81.7,
    "fluency": null,
    "rhythm": null,
    "integrity": null
  },
  "words": [
    {
      "word": "卖",
      "pinyin": "mai4",
      "score": 96.4,
      "start_ms": 423,
      "end_ms": 645,
      "phones": [
        {
          "dimension": "initial",
          "expected": "m",
          "heard": "m",
          "score": 96.7,
          "decision": "no_correction",
          "flagged": false
        },
        {
          "dimension": "final",
          "expected": "ai",
          "heard": "ai",
          "score": 98.1,
          "decision": "no_correction",
          "flagged": false
        },
        {
          "dimension": "tone",
          "expected": 4,
          "heard": 3,
          "score": 60,
          "decision": "uncertain",
          "flagged": false
        }
      ]
    }
  ],
  "syllables": [
    {
      "index": 2,
      "expected": {
        "pinyin": "mai4",
        "initial": "m",
        "final": "ai",
        "tone": 4
      },
      "span_ms": [
        423,
        645
      ],
      "score": 96.4,
      "components": {
        "initial": {
          "expected": "m",
          "heard": "m",
          "agrees_with_prompt": true,
          "score": 96.7,
          "confidence": "high",
          "decision": "no_correction",
          "advice": null
        },
        "final": {
          "expected": "ai",
          "heard": "ai",
          "agrees_with_prompt": true,
          "score": 98.1,
          "confidence": "high",
          "decision": "no_correction",
          "advice": null
        },
        "tone": {
          "expected": 4,
          "heard": 3,
          "agrees_with_prompt": false,
          "score": 60,
          "confidence": "high",
          "alternatives": [
            {
              "label": 4,
              "probability": 0.0004
            },
            {
              "label": 3,
              "probability": 0.9986
            },
            {
              "label": 2,
              "probability": 0.0008
            },
            {
              "label": 1,
              "probability": 0.0002
            }
          ],
          "decision": "uncertain",
          "advice": null
        }
      }
    }
  ],
  "heard": {
    "syllables": [
      {
        "initial": "m",
        "final": "ai",
        "tone": 3
      }
    ]
  },
  "duration_seconds": 0.989,
  "billing": {
    "credits": 1,
    "balance_credits": 2499
  }
}

The tone was heard as a 3rd (probability 0.9986) where a 4th was expected, and its grade dropped to 60 while the initial and final stayed above 96. The decision is uncertain, not review: the correction policy only says review when its evidence for an error is strong, and the display grade and the decision answer different questions. A practical UI shows the expected and heard tone for review, offers a gentle hint for uncertain, and stays quiet for no_correction.

Reading tones in code

Python
for word in result["words"]:
    tone = next(p for p in word["phones"] if p["dimension"] == "tone")
    if tone["decision"] == "review":
        print(f'{word["word"]}: expected tone {tone["expected"]}, heard tone {tone["heard"]}')
    elif tone["decision"] == "uncertain":
        print(f'{word["word"]}: listen again to the tone ({tone["score"]})')

Tone sandhi

When a 3rd tone precedes another 3rd tone it is said as a 2nd (nǐ hǎo → ní hǎo), and 不 becomes 2nd tone before a 4th. The engine expects both changes, so a learner who applies them is not told the tone is wrong: in the quickstart response, 你 is heard as tone 2 with decision no_correction. The 一 change is deliberately not applied. Details in tone sandhi in pronunciation scoring.

Request

cURL
curl https://api.toneperfect.app/v1/assess \
  -H "Authorization: Bearer $TONEPERFECT_API_KEY" \
  -H "Idempotency-Key: lesson-42-attempt-1" \
  -F "audio=@take.wav" \
  -F "text=我想卖书" \
  -F "language=zh-CN" \
  -F "task=connected_speech"

Known weak spots of the current Mandarin model: the neutral tone is not reliably told apart from a 1st tone, ü / i is the least sensitive final, and a retroflex said where a dental was expected (sh for s) is caught far less often than the reverse. The pack is labelled experimental in /v1/capabilities, and every response names the model_version that scored it. More on the Mandarin pack page and the methodology.