npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kensio/pinyinjs

v1.5.2

Published

Chinese hanzi to pinyin conversion

Downloads

947

Readme

pinyinjs

Hanzi → pinyin conversion for TypeScript, in Node and the browser. Also parses, validates and writes pinyin syllables on their own, with no dictionary.

https://pinyinjs.dev

Install

pnpm add @kensio/pinyinjs

Node 22+, or any browser. The core imports no Node built-ins, and the package is ESM only.

The dictionaries ship inside the package, which is what makes it a 4 MB download: data/ is 10 MB of artifacts and the point of the whole thing.

Command line

Installing the package installs a pinyinjs command, which is the quickest way to try any of this.

$ pinyinjs convert 我要去北京。
Wǒ yào qù Běijīng.

$ pinyinjs convert --notation numbers 银行
yin2hang2

$ pinyinjs slug 我想学中文。
wo3-xiang3-xue2-zhong1wen2

$ pinyinjs script 我们后来发现了头发问题 --to zh-Hant
我們後來發現了頭髮問題

$ pinyinjs explain 银行
银行  yínháng
  yín     locked
  háng    word    xíng +24.6  héng +26.6  hàng +27.6

$ pinyinjs lookup 头发
头发  tóu fa  n

$ pinyinjs syllable nǐhǎo
nǐhǎo  nǐ hǎo
  nǐ        n + i, tone 3         nǐ  ni3  ni³
  hǎo       h + ao, tone 3        hǎo  hao3  hao³

$ pinyinjs convert --system wade-giles --notation none 我要去北京。
Wo yao ch'ü Pei-ching.

$ pinyinjs transcribe běijīng
běijīng     běijīng   ㄅㄟˇ ㄐㄧㄥ     pei³-ching¹ běijīng   beeijing  pei˨˩˦tɕiŋ˥

| Command | Does | | ------------ | --------------------------------------------------- | | convert | hanzi to pinyin | | html | the same, as HTML | | slug | hanzi to a URL-safe slug | | script | 简体 ↔ 繁體 conversion | | explain | each syllable, how settled it was, and what it beat | | lookup | what the dictionary holds for a word | | syllable | take written pinyin apart | | sandhi | apply tone sandhi to written pinyin | | number | read a number as Chinese numerals | | transcribe | pinyin to bopomofo, Wade-Giles, Yale, GR and IPA | | info | which dictionary is loaded, and how big it is |

Every conversion option below is a flag: --notation, --locale, --apostrophe, --capitals, --punctuation, --no-grouping, --third-tone, --no-sandhi. Run pinyinjs <command> --help for what a command takes.

A command given no arguments reads standard input, one text per line, so cat article.txt | pinyinjs convert works. syllable and sandhi need no dictionary at all and start without loading one.

At a terminal each syllable is written in its tone's colour, in MDBG's palette of red, yellow, green, blue, and the terminal's own colour for the neutral tone. It is off for a pipe, NO_COLOR is honoured, --colour and --no-colour force it either way, and --json is never coloured. See the command line.

Calling it from something else

convert writes the pinyin and nothing else, so it drops straight into a pipeline:

$ pinyinjs convert 银行
yínháng

Everything else has columns for a person to read. Add --json to any command and it writes one JSON document per answer instead, which is what jq wants:

$ pinyinjs explain 长江大桥 --json | jq -c '.syllables[] | select(.state != "locked")'
{"text":"Cháng","state":"word","tone":2,"alternatives":[{"reading":"zhǎng","cost":24.62}]}
{"text":"Dà","state":"word","tone":4,"alternatives":[{"reading":"dài","cost":22.62}]}

$ pinyinjs lookup 垃圾 --json | jq -r .taiwanReading
lè sè

$ cat article.txt | pinyinjs convert --json | jq -r .pinyin

One document per answer rather than one array for the whole run, so the shape is the same whether you convert one word or pipe a file through.

Load a dictionary

Converting needs a dictionary, and it is a fetchable file rather than a JavaScript module, so loading it is asynchronous.

import { convert, loadDictionary } from "@kensio/pinyinjs";
import { fileSource } from "@kensio/pinyinjs/node";

const source = fileSource("node_modules/@kensio/pinyinjs/data");
const dictionary = await loadDictionary(source, "full");

convert(dictionary, "银行"); // "yínháng"

In a browser, serve the package's data/ directory and fetch it:

import { convert, fetchSource, loadDictionary } from "@kensio/pinyinjs";

const dictionary = await loadDictionary(fetchSource("/data"), "standard");
convert(dictionary, "长城"); // "Chángchéng"

Serve the artifacts uncompressed and let HTTP Content-Encoding: br compress them: DecompressionStream has no brotli.

Load the dictionary once and keep it; it is immutable and safe to share.

Tiers

| Tier | Entries | Download (brotli) | Contains | | ---------- | ------: | ----------------: | ---------------------- | | core | 16,730 | 70 KB | single characters only | | standard | 66,730 | 376 KB | the most common words | | full | 461,623 | 2,381 KB | every word |

full is the default. The tiers are nested, so a page can load standard first, convert with it, and reload as full arrives.

Convert

convert(dictionary, "银行"); // "yínháng"
convert(dictionary, "行长"); // "hángzhǎng"
convert(dictionary, "我要去北京。"); // "Wǒ yào qù Běijīng."
convert(dictionary, "3D银行"); // "sān D yínháng", the digit is read, the letter is not

A reading the dictionary cannot settle on its own is settled by context, with typed rules over the lattice rather than tweaks to the output:

convert(dictionary, "我得走了"); // "wǒ děi zǒule", modal 得
convert(dictionary, "他跑得很快"); // "tā pǎo de hěn kuài", the particle
convert(dictionary, "那边儿"); // "nà biānr", 儿 does not stand on its own

See converting.

Options

convert(dictionary, text, { notation: "numbers", capitals: "none" });

| Option | Default | Values | | ------------- | ---------------------------------- | ------------------------------------------------- | | locale | "zh-CN" | "zh-CN", "zh-TW" | | notation | "marks" | "marks", "numbers", "superscript", "none" | | apostrophe | "always" | "always", "standard", "never" | | capitals | "auto" | "auto", "proper", "none" | | punctuation | "latin" | "latin", "keep" | | grouping | true | false turns off GB/T 16159 word spacing | | numbers | "read" | "keep" leaves every digit as it was written | | sandhi | { yiBu: true, thirdTone: false } | { yiBu?: boolean; thirdTone?: boolean } |

convert(dictionary, "垃圾"); // "lājī"
convert(dictionary, "垃圾", { locale: "zh-TW" }); // "lèsè"
convert(dictionary, "银行", { notation: "numbers" }); // "yin2hang2"
convert(dictionary, "银行", { notation: "superscript" }); // "yin²hang²"
convert(dictionary, "银行", { notation: "none" }); // "yinhang"
convert(dictionary, "西安"); // "Xī'ān"
convert(dictionary, "海鸥", { apostrophe: "standard" }); // "hǎiōu"
convert(dictionary, "北京。", { punctuation: "keep" }); // "Běijīng。"
convert(dictionary, "北京。", { capitals: "none" }); // "běijīng."
convert(dictionary, "好好", { sandhi: { thirdTone: true } }); // "háohǎo"

Spacing, capitals and punctuation

Capitals. Proper nouns always; the first word of a sentence only when the source is punctuated as one, since that is the only thing separating 学生 looked up as a word from 这是我的书。written as a sentence. A comma does not count.

convert(dictionary, "银行"); // "yínháng", not "Yínháng"
convert(dictionary, "我要去北京。"); // "Wǒ yào qù Běijīng."
convert(dictionary, "你好,世界"); // "nǐ hǎo, shìjiè"

Apostrophes. The 隔音符号 goes before any syllable of a word that starts with a, o or e and is not the first. apostrophe: "standard" writes it only where leaving it out would read as something else.

convert(dictionary, "天安门"); // "Tiān'ānmén"
convert(dictionary, "女儿"); // "nǚ'ér"

Punctuation. 。,、;:?! are rewritten as their Latin equivalents and take the space the full-width glyph carried. Brackets and quotation marks are left alone. punctuation: "keep" leaves everything as it was.

Word spacing. 分词连写 is applied to the decoded words: aspect particles attach to their verb, suffixes to their stem, and the generic half of a place name separates and capitalises. A small curated list covers words the standard writes in a way no rule reaches.

convert(dictionary, "他看了"); // "tā kànle"
convert(dictionary, "我还给你了。"); // "Wǒ huán gěi nǐ le.", sentence-final 了
convert(dictionary, "作者"); // "zuòzhě"
convert(dictionary, "南京市"); // "Nánjīng Shì"
convert(dictionary, "南京市", { grouping: false }); // "Nánjīngshì"
convert(dictionary, "不是"); // "bú shì"
convert(dictionary, "一个"); // "yí gè"
convert(dictionary, "黄河"); // "Huáng Hé"
convert(dictionary, "中国人"); // "Zhōngguórén"

The list is not a complete 正词法 implementation, so some words it does not cover are written differently: 不但 is búdàn, 大米 is dàmǐ, 青海 is Qīnghǎi.

Reduplication takes a hyphen rather than a space, since it is one word with a boundary inside it.

convert(dictionary, "干干净净"); // "gāngān-jìngjìng"
convert(dictionary, "研究研究"); // "yánjiū-yánjiū"
convert(dictionary, "爸爸妈妈"); // "bàba māma", that shape, but two words

A 成语 that can be read as two disyllables takes the same hyphen, from a curated list of 117; the rest are written solid, as the standard writes them.

convert(dictionary, "风平浪静"); // "fēngpíng-làngjìng"
convert(dictionary, "不亦乐乎"); // "búyìlèhū", cannot be halved

Digits are read, and the rest of a non-Han run passes through exactly as written:

convert(dictionary, "我有3个苹果。"); // "Wǒ yǒu sān gè píngguǒ."
convert(dictionary, "1988年之后"); // "yī jiǔ bā bā nián zhīhòu"
convert(dictionary, "95%的人"); // "bǎifēnzhījiǔshíwǔ de rén"
convert(dictionary, "3D打印"); // "sān D dǎyìn"
convert(dictionary, "6:30起床"); // "liù diǎn sānshí fēn qǐchuáng"
convert(dictionary, "16:9的"); // "16:9de", a ratio is not a quantity

numbers: "keep" leaves every digit alone. See numbers.

Syllable by syllable

convertPieces returns the same conversion one piece at a time, with the syllable behind each and what the decoder chose it over.

import { convertPieces, isUncertain, writeSyllable } from "@kensio/pinyinjs";

const pieces = convertPieces(dictionary, "银行");
pieces.map((piece) => piece.text); // ["yín", "háng"]
pieces[1]?.syllable; // { initial: "h", final: "ang", tone: 2 }
pieces[0]?.confidence?.isLocked; // true, nothing else can be read here
pieces[1]?.confidence?.alternatives.map((found) =>
  found.reading.map((syllable) => writeSyllable(syllable)).join(""),
); // ["xíng", "héng", "hàng"]

A piece with no syllable is the text between two of them: a space, or a run that was never Han. joinPieces(pieces) gives back exactly what convert returns.

Each syllable is in one of three states:

| State | isLocked | isUncertain | Meaning | | ---------------- | ---------- | ------------- | ---------------------------------------------------------- | | locked | true | false | only one reading is possible here | | backed by a word | false | false | other readings exist; taking one means breaking a word up | | uncertain | false | true | another reading of the same characters was nearly as cheap |

const guesses = (text: string) =>
  convertPieces(dictionary, text).filter(
    (piece) => piece.confidence !== undefined && isUncertain(piece.confidence),
  );

guesses("行").map((piece) => piece.text); // ["xíng"], nothing but a prior chose it
guesses("银行").map((piece) => piece.text); // [], the word settles both syllables

An alternative's cost says how much more the cheapest conversion taking it would have cost, in the decoder's own units. Treat it as a measure of how much evidence there was, not as a probability.

HTML output

import { convertToHtml } from "@kensio/pinyinjs";

convertToHtml(dictionary, "行");
// <span class="py-syllable py-tone-2 py-uncertain" lang="zh-Latn-CN-pinyin"
//       data-alternatives="háng héng hàng">xíng</span>

One element per syllable, with py-tone-1 to py-tone-5 (5 is the neutral tone), and py-uncertain plus the rejected readings where the decoder was guessing. Each one declares itself pinyin — zh-Latn-TW-pinyin for a zh-TW conversion — so that a screen reader does not read it as the language of the page around it. Text that is not Han is escaped, not marked up. No styles are included, so write your own:

.py-tone-1 {
  color: #c1272d;
}
.py-uncertain {
  border-bottom: 1px dotted currentcolor;
}

Takes any convert option, plus toneClasses: false, markUncertain: false and lang: false. toHtml(pieces, options) renders pieces you already have.

Look words up

const entry = dictionary.lookup("头发");
entry?.reading; // [{ initial: "t", final: "ou", tone: 2 }, { initial: "f", final: "a", tone: 5 }]
entry?.isProperNoun; // false
entry?.partOfSpeech; // "n", jieba's tag

dictionary.lookup("頭髮")?.reading; // the same reading, found under 繁體
dictionary.lookup("重複")?.reading; // 重複 and 重覆 are both keys for 重复
dictionary.hasPrefix("银"); // true, does any word start with this?
dictionary.readingsOf("行"); // xíng, háng, héng, hàng, likeliest first

Both scripts are keys in the same dictionary, so nothing is converted before a lookup.

Syllables

The syllable layer needs no dictionary and no network.

import { isSyllable, readSyllable, writeSyllable } from "@kensio/pinyinjs";

readSyllable("jiù"); // { initial: "j", final: "iou", tone: 4 }
readSyllable("jiu4"); // the same, both notations parse
readSyllable("lv4"); // { initial: "l", final: "ü", tone: 4 }
readSyllable("hello"); // undefined
readSyllable("běi3"); // undefined, one notation at a time

isSyllable("wánr"); // true, 儿化 is a suffix, not a syllable of its own

Initials and finals are the underlying forms rather than the spelling, so 就 is j + iou and 军 is j + ün. Spelling is reconstructed on demand:

const jiu = { initial: "j", final: "iou", tone: 4 } as const;
writeSyllable(jiu); // "jiù"
writeSyllable(jiu, "numbers"); // "jiu4"
writeSyllable(jiu, "superscript"); // "jiu⁴"
writeSyllable(jiu, "none"); // "jiu"

Input takes either notation, the v and u: conventions for ü, and raised tone digits. Output is standard diacritics unless asked otherwise.

Parsing answers whether a spelling is well formed, not whether Mandarin uses it: shong parses and is not a real syllable. The attested inventory is separate:

import { ATTESTED_SYLLABLES, DICTIONARY_SYLLABLES } from "@kensio/pinyinjs";

DICTIONARY_SYLLABLES.has("shong"); // false
DICTIONARY_SYLLABLES.has("zhuang"); // true
ATTESTED_SYLLABLES.length; // 415

Splitting written pinyin

import { readWord, splitSyllables } from "@kensio/pinyinjs";

splitSyllables("nǐhǎo"); // ["nǐ", "hǎo"]
splitSyllables("Xī'ān"); // ["Xī", "ān"]
splitSyllables("yinhang"); // ["yin", "hang"]
splitSyllables("guórén"); // ["guó", "rén"], not ["guór", "én"]
splitSyllables("hǎiōu"); // ["hǎi", "ōu"], missing apostrophe, read anyway
readWord("yínháng"); // the same, parsed into Syllable objects

Tones

import {
  applyToneMark,
  NEUTRAL_TONE,
  stripToneMarks,
  toneFromMarks,
} from "@kensio/pinyinjs";

applyToneMark("hao", 3); // "hǎo"
applyToneMark("hao", NEUTRAL_TONE); // "hao"
stripToneMarks("hǎo"); // "hao"
toneFromMarks("hǎo"); // 3

Syllable.tone is Tone | undefined, and undefined is not the neutral tone: the de in 我的 is neutral (5), whereas the bei in a typed beijing has no tone written at all.

Numbers

Reading a number needs no dictionary, just arithmetic and about twenty readings, so this works with nothing loaded.

import { numeralHanzi, percentHanzi, readNumeral } from "@kensio/pinyinjs";

numeralHanzi(12345); // "一万两千三百四十五"
numeralHanzi(1005); // "一千零五", a skipped place is spoken
numeralHanzi(2000); // "两千", a leading lone 2 before a big unit
numeralHanzi(2, { counts: true }); // "两", as in 两个西瓜, never 二个
percentHanzi(95); // "百分之九十五", the order reverses

The same digits are read two ways and nothing in the number says which, since 2026年 is spelled out and 2026个 is counted, so the style is the caller's:

numeralHanzi(2026); // "两千零二十六"
numeralHanzi(2026, { style: "digits" }); // "二〇二六"
readNumeral(110, { style: "digits", yao: true }); // yāo yāo líng

More in numbers.

Bopomofo, Wade-Giles, Yale, Gwoyeu Romatzyh and IPA

Also dictionary-free: a romanisation is a mapping over about 420 syllables, so hanzi → Wade-Giles is hanzi → pinyin → Wade-Giles.

import {
  readSyllable,
  readWadeGilesLoosely,
  writeBopomofo,
  writeGwoyeu,
  writeIpa,
  writeWadeGiles,
  writeYale,
} from "@kensio/pinyinjs";

const jiu = readSyllable("jiù");
writeBopomofo(jiu); // "ㄐㄧㄡˋ"
writeWadeGiles(jiu); // "chiu⁴"
writeYale(jiu); // "jyòu"
writeGwoyeu(jiu); // "jiow"
writeIpa(jiu); // "tɕiou˥˩"

Gwoyeu Romatzyh is the odd one, and it needs no tone mark because the tone is spelled into the syllable, which is why 陝西 is Shaanxi in English and 山西 is Shanxi:

const gr = (pinyin: string) => writeGwoyeu(readSyllable(pinyin));
[gr("shān"), gr("shán"), gr("shǎn"), gr("shàn")]; // shan, sharn, shaan, shann

Reading Wade-Giles back gives an array, because real text drops the apostrophes and diacritics that carry the distinctions:

readWadeGilesLoosely("chi¹"); // [jī, qī]: chi is jī, ch'i is qī
readWadeGilesLoosely("chu¹"); // [zhū, chū, jū, qū]

Measured over the phrase corpus, 52.07% of written syllables have a Wade-Giles spelling that merges with another once its marks are dropped, and taking the first candidate recovers 79.05% of them. More in romanisation.

Sandhi

The dictionary stores underlying tones, and sandhi is applied to the syllable array, so it can be switched off and works across word boundaries.

import { applySandhi, readWord } from "@kensio/pinyinjs";

const buShi = readWord("bùshì") ?? [];
applySandhi(buShi); // bú shì, 不 flattens before a fourth tone
applySandhi(buShi, { yiBu: false }); // unchanged

const niHao = readWord("nǐhǎo") ?? [];
applySandhi(niHao); // unchanged by default
applySandhi(niHao, { thirdTone: true }); // ní hǎo

Third-tone sandhi is off by default: standard orthography writes 你好 as nǐ hǎo even though it is said ní hǎo. Turn it on for transcribing speech. Its domain is the prosodic foot rather than the syllable string, so 展览馆 is zhánlánguǎn and 纸老虎 zhǐláohǔ, and 老板很好 keeps the 板 that a left-to-right scan would lower.

Scripts and locales

Three axes, not one. Taiwan writes 繁體 with zh-TW readings, but mainland editions of classical texts use 繁體 with zh-CN readings, and Singapore uses 简体.

| Axis | Values | What differs | | ----------- | ----------------- | ------------------------------- | | Script | Hans / Hant | which characters are written | | Glyph forms | TW / HK | which 繁體 standard writes them | | Locale | zh-CN / zh-TW | how they are read |

Both scripts are dictionary keys, so only the locale is an option to pass. Hong Kong's 繁體 forms are recognised on the way in, so 羣眾 and 麪包 read exactly as 群眾 and 麵包 do.

Simplified and traditional

toScript converts between the scripts, and the reading is what makes it accurate. Simplification merged distinct characters; the reading un-merges them.

import { loadScriptTables, toScript } from "@kensio/pinyinjs";

const tables = await loadScriptTables(source);

toScript(dictionary, tables, "我们后来发现了头发问题", { to: "zh-Hant" });
// "我們後來發現了頭髮問題"

toScript(dictionary, tables, "头发", { to: "zh-Hant" }); // "頭髮", tóufà
toScript(dictionary, tables, "出发", { to: "zh-Hant" }); // "出發", chūfā

发 is 發 or 髮 and nothing about the character says which. Every other converter works from phrase tables alone, so it is right about the words on the list and guessing past it.

It runs both ways — 乾燥 is gānzào and simplifies to 干燥, while 乾隆 is Qiánlóng and stays 乾隆 — and it targets a region, because there is no region-free 繁體:

toScript(dictionary, tables, "面包", { to: "zh-Hant-TW" }); // "麵包"
toScript(dictionary, tables, "面包", { to: "zh-Hant-HK" }); // "麪包"

toScriptPieces reports what settled each character, and which were guesses: 下面 is a surface or a bowl of noodles, both xiàmiàn, and it says so rather than picking silently. The tables load separately from the dictionary, so converting hanzi to pinyin costs nothing for them.

This is orthography and not translation — 软件 becomes 軟件, never 軟體. See script conversion.

API

| Function | Does | | ---------------------------------------------------- | ------------------------------------------------- | | loadDictionary(source, tier) | load a dictionary from fileSource/fetchSource | | convert(dictionary, text, options?) | hanzi → pinyin | | convertPieces(dictionary, text, ...) | the same, per syllable, with confidence | | convertToHtml(dictionary, text, ...) | the same, as HTML | | slug(dictionary, text, options?) | hanzi → a URL-safe slug | | joinPieces(pieces) / toHtml(pieces) | render pieces | | isUncertain(confidence) | was this syllable a guess? | | dictionary.lookup / hasPrefix / readingsOf | query the dictionary | | readSyllable / writeSyllable / isSyllable | one syllable, no dictionary | | splitSyllables / readWord | split written pinyin | | applySandhi | 一, 不 and optional third-tone sandhi | | writeBopomofo / writeWadeGiles | one syllable, romanised | | writeYale / writeGwoyeu / writeIpa | the same, in Yale, GR and IPA | | readBopomofo / readWadeGilesLoosely | and back again | | readYale / readGwoyeu / readIpa | and back from those three | | applyToneMark / stripToneMarks / toneFromMarks | tone marks | | convertGreedily(...) | the old longest-match decoder, kept as a baseline |

Types (Syllable, Tone, ConvertOptions, ConvertedPiece, ReadingConfidence, HtmlOptions, WordEntry, Tier, Locale, Script) are exported alongside them.

Development

pnpm install
pnpm check      # format, complexity, build, typecheck, test with coverage
./pinyinjs      # the CLI, straight from the sources: ./pinyinjs convert 你好
pnpm accuracy   # score both decoders against the gold corpus
pnpm polyphones # score them against 20,139 hand-labelled polyphones
pnpm build:data # rebuild data/ from the upstream sources

pnpm check must pass: oxlint including the type-aware rules, tsc with exactOptionalPropertyTypes and noUncheckedIndexedAccess, and vitest at 95% coverage thresholds.

Every example in this README is executed by src/readme.test.ts against the committed dictionary, so the two cannot drift apart. Change them together.

The compiled dictionaries in data/ are committed, so what ships is exactly what was tested. pnpm build:data fetches the four sources into .cache/ (~32 MB), merges them, runs the build assertions, and rewrites data/ and NOTICE. It fails rather than warns: no artifact is written unless 儿化 is repaired both ways, 一 and 不 sandhi is normalised out, every syllable is one the inventory knows, and every tier reads back exactly as it was built.

Data sources

| Source | Provides | Licence | | --------------------------------------------------------------------- | ----------------------------------------------------- | ------------ | | Unihan | character readings, polyphone priors, script variants | Unicode | | CC-CEDICT | 简体/繁體 pairs, 儿化, neutral tones, Taiwan readings | CC BY-SA 4.0 | | phrase-pinyin-data | the bulk of the word readings | MIT | | jieba | word frequencies and part-of-speech tags | MIT |

NOTICE is generated from the same table the pipeline fetches from.

Licence

Apache-2.0 AND CC-BY-SA-4.0, because the package is two things: the code is Apache-2.0, and the compiled dictionaries in data/ are share-alike, CC-CEDICT being CC BY-SA 4.0. See LICENSE and NOTICE, which is generated from the sources the pipeline actually fetched.