Checking typed pinyin
check compares typed pinyin with Chinese text and reports a verdict for each
syllable.
import { check } from "@kensio/pinyinjs";
const marked = check(dictionary, "银行", "yínxíng");marked.syllables.map((one) => one.verdict); // ["correct", "wrong"]marked.syllables[1]?.source; // "行"marked.score; // 0.5Use it to mark pronunciation exercises and highlight missing syllables, wrong readings or tone mistakes.
Accepted variations
Section titled “Accepted variations”The checker accepts several ways to write the same reading.
Tone marks and tone numbers can be mixed within a word. The
syllable parser treats bei3 and běi as the same syllable:
check(dictionary, "北京", "běijīng").isCorrect; // truecheck(dictionary, "北京", "bei3jing1").isCorrect; // truecheck(dictionary, "北京", "bei3jīng").isCorrect; // trueWhen the decoder is uncertain, the checker accepts its plausible alternatives.
For 行 alone, this includes xíng, háng and héng:
check(dictionary, "行", "xíng").isCorrect; // truecheck(dictionary, "行", "háng").isCorrect; // trueA reading that breaks a recognised word is rejected. For example, the 行 in
银行 must be háng:
check(dictionary, "银行", "yínxíng").syllables[1]?.verdict; // "wrong"This uses the same threshold as isUncertain. An alternative
is accepted if choosing it would cost less than one word boundary.
Both underlying tones and sandhi forms are accepted. 你好 can be
nǐ hǎo or ní hǎo, and 不是 can be bù shì or bú shì:
check(dictionary, "你好", "nǐ hǎo").isCorrect; // truecheck(dictionary, "你好", "ní hǎo").isCorrect; // truecheck(dictionary, "不是", "bú shì").isCorrect; // truecheck(dictionary, "不是", "bù shì").isCorrect; // trueAn omitted tone is reported separately from a wrong tone:
check(dictionary, "北京", "bei jing").syllables.map((one) => one.verdict);// ["toneless", "toneless"]check(dictionary, "北京", "bei3jing3").syllables.map((one) => one.verdict);// ["correct", "tone"]Apostrophes mark syllable boundaries. The checker accepts their omission when the syllables are still unambiguous:
check(dictionary, "西安", "Xī'ān").isCorrect; // truecheck(dictionary, "西安", "xi1an1").isCorrect; // truecheck(dictionary, "海鸥", "hǎiōu").isCorrect; // true, the mark is optional thereFor 西安, xiān is wrong because it is one syllable. Xīān passes because
the two tone marks identify two syllables.
Word spacing is reported separately. See word spacing.
Syllable verdicts
Section titled “Syllable verdicts”The result contains one entry per expected or typed syllable, in order.
| Verdict | Means |
|---|---|
correct |
right syllable, right tone |
toneless |
right syllable, no tone written |
tone |
right syllable, wrong tone |
wrong |
wrong syllable |
missing |
a syllable of the reading that was not typed |
extra |
a syllable typed that the reading does not use |
The spacing field reports word spacing separately from the syllable verdict.
An unmarked neutral-tone syllable is correct. Pinyin normally writes the
neutral tone without a mark:
check(dictionary, "我的书", "wǒ de shū").isCorrect; // trueRequiring tones
Section titled “Requiring tones”By default, toneless counts as correct. Set tones: "required" to count it
as a mistake. The verdict remains toneless in either mode.
check(dictionary, "北京", "bei jing").isCorrect; // truecheck(dictionary, "北京", "bei jing", { tones: "required" }).isCorrect; // falseWord spacing
Section titled “Word spacing”The spacing field reports whether word boundaries match. For example,
yín háng has the right syllables for 银行 but splits one word into two:
const split = check(dictionary, "银行", "yín háng");split.syllables.map((one) => one.verdict); // ["correct", "correct"]split.syllables.map((one) => one.spacing); // ["correct", "split"]| Spacing | Means |
|---|---|
correct |
a word begins here, or does not, as it should |
split |
a word was written as two |
joined |
two words were written as one |
spacing is undefined for a missing or extra syllable because it has no
counterpart to compare.
Spacing is always reported. Set spacing: "required" to include it in the score:
check(dictionary, "银行", "yín háng").isCorrect; // truecheck(dictionary, "银行", "yín háng", { spacing: "required" }).isCorrect; // falseSpacing is excluded from the score by default. The orthography implementation covers only part of the standard, and some valid spellings may differ from its output. Enable spacing checks when the exercise is specifically testing word boundaries.
Accepted boundaries
Section titled “Accepted boundaries”With spacing: "required", the checker accepts these variations:
const graded = { spacing: "required" } as const;Both orthographic grouping and dictionary word boundaries are accepted.
Orthographic grouping joins aspect particles to verbs and separates the generic
part of a place name. grouping: false uses dictionary words directly:
check(dictionary, "他看了", "tā kànle", graded).isCorrect; // true, 分词连写check(dictionary, "他看了", "tā kàn le", graded).isCorrect; // true, the wordscheck(dictionary, "南京市", "Nánjīng Shì", graded).isCorrect; // truecheck(dictionary, "南京市", "Nánjīngshì", graded).isCorrect; // trueA hyphenated word can be written with a space or joined without the hyphen.
For example, 干干净净 is normally gāngān-jìngjìng:
check(dictionary, "干干净净", "gāngān-jìngjìng", graded).isCorrect; // truecheck(dictionary, "干干净净", "gāngān jìngjìng", graded).isCorrect; // truecheck(dictionary, "干干净净", "gāngānjìngjìng", graded).isCorrect; // trueOther missing or extra boundaries are reported as spacing mistakes:
check(dictionary, "我要去北京。", "wǒyàoqùběijīng", graded).syllables.map( (one) => one.spacing,); // ["correct", "joined", "joined", "joined", "correct"]Syllable details
Section titled “Syllable details”| Field | Is |
|---|---|
verdict |
one of the six above |
spacing |
correct, split or joined |
isCorrect |
whether it counts as right in the score |
expected |
the Syllable expected here, or undefined for an extra |
actual |
the Syllable typed here, or undefined for a missing |
text |
what was typed, exactly as written |
source |
the characters the expected syllable reads |
at |
where those characters start, in code points |
Use source and at to highlight the characters associated with a mistake.
const marked = check(dictionary, "我要去银行", "wǒ yào qù yínxíng");marked.syllables .filter((one) => !one.isCorrect) .map((one) => [one.source, one.at, one.text]);// [["行", 4, "xíng"]]text preserves what the learner typed. If it cannot be parsed as a syllable,
actual is undefined and the verdict is wrong.
score is the proportion of reported syllables counted as correct, from 0 to 1.
isCorrect is true when every reported syllable counts as correct.
The denominator includes extra typed syllables. Adding a syllable and omitting a syllable both reduce the score.
check(dictionary, "北京", "běi běi jīng").score; // 2/3check(dictionary, "北京市", "běi shì").score; // 2/3Alignment
Section titled “Alignment”The checker aligns expected and typed syllables by their toneless spellings before grading them. A missing syllable does not shift every subsequent syllable into the wrong position:
check(dictionary, "北京市", "běi shì").syllables.map((one) => one.verdict);// ["correct", "missing", "correct"]Unmatched syllables within each gap are paired as substitutions. For 银行 typed
as yínxíng, yín aligns and xíng is one wrong syllable in place of háng.
Options
Section titled “Options”check accepts every conversion option, plus tones and
spacing to control scoring.
| Option | Default | Values |
|---|---|---|
tones |
"optional" |
"optional", "required" |
spacing |
"optional" |
"optional", "required" |
Use readings when the exercise requires a particular pronunciation:
check(dictionary, "这篇文章不太长。", "zhè piān wénzhāng bú tài cháng", { readings: { 太长: "tài cháng" },}).isCorrect; // truelocale: "zh-TW" grades against the 國語 reading, so 垃圾 reads lèsè where
zh-CN reads lājī.
At the command line
Section titled “At the command line”$ pinyinjs check 银行 yínxíng银行 yínháng 50% 银 yín yín correct 行 háng xíng wrong
$ pinyinjs check 银行 "yín háng" --require-spacing银行 yínháng 50% 银 yín yín correct 行 háng háng correct splitThe CLI prints the text, expected reading and score, followed by each syllable with its source characters, expected reading, typed text and verdict. Spacing mistakes appear beside the affected syllables.
The first argument is Chinese text. Remaining arguments are joined as the typed
pinyin, so pinyinjs check 北京市 běijīng shì works without quoting the pinyin.
For standard input, provide one tab-separated pair per line:
$ printf '银行\tyínxíng\n北京\tbei3jing3\n' | pinyinjs checkUse --require-tones and --require-spacing to include them in the score.
Conversion flags also apply. For example, --locale zh-TW checks 國語 readings.
--json includes every result field, including at and source.
Conversion cost
Section titled “Conversion cost”Each check performs three conversions. The first supplies the expected reading and its confidence report. The second reverses both sandhi settings to obtain the alternative tone forms. These two conversions cover all four sandhi combinations because 一/不 sandhi and third-tone sandhi affect different syllables. The third conversion supplies the alternative word spacing.
