Misakid

Misakid is a platform-neutral Dart port of hexgrad/misaki, the grapheme-to-phoneme (G2P) engine used by Kokoro models. Implemented parity slices are pinned to Misaki 0.9.4 at commit fba1236595f2d2bf21d414ba6e57d25256afada3; the precise slices identified in PORTING_STATUS.md are checked with exact JSONL fixtures exported from Python.

Python is used only by repository-local development tooling: the pinned upstream oracle, read-only fixture verification, and deterministic generated- data checks. The published root library excludes those scripts, fixture tests, Python environments, and root fixture-generator inputs; sibling adapter build manifests needed by their published offline builders remain included. Conversion and consumer use require no Python; the full repository test suite does provision Python to check generator reproducibility.

This package is under active pre-1.0 development. Japanese is the primary mobile target: the Kokoro-default Cutlet mode now has a vendored, reproducible MeCab native-assets build for Android and iOS plus byte-backed grouping data. The same bundled build passes the complete pinned Cutlet oracle on macOS, an Android 15/API 35 arm64 emulator, and an iOS 26.4 Simulator. Three-ABI Android release artifacts and an unsigned iOS device app also pass native packaging checks. Physical-device validation and the clean hosted mobile matrix remain formal release gates. Japanese pyopenjtalk-style and Korean g2pkc-default modes remain supported for their precise macOS arm64 adapter configurations. Chinese legacy/default, frontend-1.1 without an English callback, and the exact frontend-1.1 small-model/no-fallback English callback profile are supported by explicit pure-Dart resource packages. English American and British small-model modes are supported without fallback by a pure-Dart tokenizer/tagger using explicit en_core_web_sm==3.8.0 resources; their eSpeak-fallback variants are supported on macOS arm64 with a second explicit native adapter. English transformer no-fallback and eSpeak-fallback modes are also supported on macOS arm64 with the exact external en_core_web_trf==3.8.0 resources and package-owned Accelerate adapter. PeterReid BART fallback and other complete language modes remain unsupported. See PORTING_STATUS.md for the precise capability matrix.

Kokoro and ONNX handoff

Kokoro 0.9.4 defaults Japanese to Misaki's JAG2P(), whose default frontend is Cutlet. JapaneseCutletEngine plus misakid_mecab_ja implements that G2P path. KokoroNonEnglishG2pFrontend additionally reproduces Kokoro's line splitting, roughly 400-code-point pre-G2P packing, and 510-code-point phoneme limit. KokoroEnglishG2pFrontend implements the distinct token-aware English punctuation waterfall. Kokoro constructs its English G2P with unk='', so the frontend accepts only an UnknownMarkerG2pEngine whose actual unknownMarker is empty; construct EnglishG2pEngine with unknownMarker: ''. Both frontends return immutable KokoroG2pChunk values whose nullable textIndex preserves Kokoro's original default line-feed segment index, including skipped segments and repeated indices for subchunks.

The accepted eleven-case Kokoro fixture executes the unmodified pinned kokoro/pipeline.py under dependency-rejecting stdlib stubs. It covers the English 510/511 boundaries, all punctuation-waterfall levels and fallthrough, the fixed unknown policy, non-English 400/401 packing, segment indices, valid supplementary characters, and isolated UTF-16 surrogates.

This repository deliberately stops at the G2P boundary. A separate ONNX inference library consumes each chunk's phonemes, applies the selected Kokoro model's vocabulary (including filtering unmapped symbols and adding BOS/EOS IDs), selects voice/style data, constructs tensors, and runs inference. No ONNX Runtime, model weight, voice pack, or platform inference dependency is introduced here.

English pipelines

The package includes an offline pinned American/British pronunciation provider with contextual lexicon, morphology, currency, and number behavior. It can be used directly:

import 'package:misakid/misaki_en.dart';

void main() {
  const lexicon = PinnedEnglishLexicon();
  final pronunciation = lexicon.lookup(
    const MisakiToken(
      text: 'hello',
      tag: 'NN',
      whitespace: '',
      metadata: EnglishTokenMetadata(isHead: true),
    ),
    const EnglishTokenContext(),
  );
  print(pronunciation?.phonemes); // həlˈO
}

EnglishG2pEngine composes this provider with an explicitly supplied typed tokenizer/tagger. The committed American and British no-fallback suites each replay 53 exact pinned-Python cases: the original 32-case matrix plus 21 adversarial cases covering number flags, morphology, controls, Unicode, mixed scripts, symbols, and large values. They include the legacy and 2.0 phoneme versions. Two additional accepted 20-case American/British eSpeak-fallback suites replay the exact raw eSpeak-ng calls through the public Dart fallback, covering OOV grouping, punctuation, capitalization, Unicode, dialect-specific phones, preprocessing disabled, custom unknown markers, and both phoneme versions. The compiling example/english_injected.dart shows the full public engine with a fixed tokenizer record.

englishPhonemeInventory exposes the frozen American/British inventory for either rendering version. It covers built-in lexicon/eSpeak output; custom inline pronunciations, punctuation, whitespace, and unknown markers remain caller-visible additions rather than silently accepted inventory symbols.

The sibling misakid_spacy_en package is the supported production tokenizer/tagger for the small-model configurations. It implements the pinned spaCy 3.8.4 tokenizer and Thinc 8.3.4 en_core_web_sm==3.8.0 inference path in pure Dart. Callers provide the four exact checksum-validated model files; the adapter invokes no Python, native code, subprocess, or network. Provisioned parity covers all 146 accepted raw streams, 744 tokens/tags, metadata, and final results for American and British output with legacy and 2.0 rendering. The same four exact resources can be supplied as owned byte buffers, so a Flutter asset loader or mobile model store does not need to expose desktop filesystem paths. Unknown-word eSpeak fallback remains a separate macOS-only adapter; mobile English support is therefore the exact no-fallback profile.

EnglishEspeakFallback ports the deterministic replacement and postprocessing layer behind EnglishEspeakBackend. The sibling misakid_espeak_en package supplies the supported raw eSpeak provider for the documented macOS 11+ arm64 eSpeak NG 1.52.0 resource tuple. A composed test using both real adapters matches all 40 accepted fallback cases, every token field, and all 58 ordered native calls. Neither adapter discovers, downloads, or bundles its external resources. The eSpeak adapter's byte, punctuation- split, and 65,536 native callback-chunk limits fail explicitly with typed errors; conversion has no cancellation hook and should be process-isolated when hard cancellation is required.

The sibling misakid_spacy_trf_en package supplies the supported transformer tokenizer/tagger on macOS 11+ arm64. Its Dart stages implement the exact spaCy tokenizer, RoBERTa byte-BPE, and alignment; a package-owned native library evaluates the 12-layer graph and tagger with Apple Accelerate. It invokes no Python, spaCy, Torch, subprocess, or network. Callers explicitly provide the checksum-pinned en_core_web_trf==3.8.0 model directory and build the native library from reviewed source; neither artifact is bundled or discovered.

All 116 accepted American/British transformer cases pass against the real backend: 76 no-fallback cases plus 40 eSpeak-fallback cases, covering 2,278 raw transformer token/tag records and 50 ordered raw eSpeak calls. A second composition test runs both real native adapters for all 40 fallback cases. The transformer holds approximately 496 MB of native weights, is synchronous, has no cancellation hook, and must be explicitly closed. See packages/misakid_spacy_trf_en/README.md for the build command, exact resource tuple, configurable piece bounds, and platform limits.

The two BART fallback repositories named by pinned Misaki have immutable weight checksums but empty model cards and no adequate training-data, lineage, license-file, copyright, or notice provenance. They are therefore not accepted as fixture or adapter inputs; see THIRD_PARTY_NOTICES.md for the reviewed revisions and exact blocker.

The optional misakid_bart_en package nevertheless provides an experimental pure-Dart implementation of the exact one-encoder-layer/one-decoder-layer BART architecture behind that boundary. Callers supply absolute paths plus exact byte sizes and SHA-256 identities for a JSON configuration and F32 safetensors file; the adapter never discovers, downloads, or bundles weights. Its graph is checked against a license-clean synthetic model using CPU Torch 2.6.0 and Transformers 4.51.3, including full causal decoder logits, multiple generation inputs, unknown-ID handling, and early EOS. This validates the runtime architecture only: it does not clear the PeterReid provenance blocker or make pinned Misaki's model-fallback modes supported.

Japanese number conversion

The first pure parity slice converts Arabic-number text to kanji, kana, or romaji and converts Japanese number text back to Arabic text:

import 'package:misakid/misaki_ja.dart';

void main() {
  const numbers = JapaneseNumberConverter();

  print(numbers.convert('12345')); // いちまんにせんさんびゃくよんじゅうご
  print(
    numbers.convert('20.5', format: JapaneseNumberFormat.romaji),
  ); // ni jutten go 
  print(numbers.kanjiToArabic('一億一万一')); // 100010001
}

The trailing romaji space shown above is intentional upstream behavior. Over-limit input returns upstream's literal diagnostic string; other inputs outside the pinned converter's domain fail with an InvalidConfigurationException instead of an incidental Python indexing or lookup error.

Japanese pyopenjtalk-style pipeline

JapanesePyopenjtalkEngine consumes typed morphology, pronunciation, accent, mora-count, and chain data from an explicitly supplied JapaneseFrontendBackend. Its pure renderer preserves all 193 pinned mora mappings, punctuation and whitespace behavior, unknown markers, token metadata, accent chains, and the equal-scalar-length pitch trace appended to the returned phoneme string. The 24-case committed oracle matrix replays the raw frontend word stream exactly.

japanesePhonemeInventory exposes frozen source-derived phonetic-scalar sets for the distinct Cutlet and pyopenjtalk-style contracts (35 and 40 symbols, respectively). Punctuation, configurable unknown markers, Cutlet pass-through, and pitch-only trace markers are documented exclusions.

The pure package still bundles and discovers no native frontend or dictionary. The sibling misakid_openjtalk package provides an explicit no-Python adapter for macOS arm64. Callers supply absolute native library and Open JTalk 1.11 dictionary paths; initialization streams and validates the exact source/ABI/dictionary identities before opening. The new owned-result ABI passed all 24 cases, all 155 raw words and 14 fields, all 23 successful final results and typed token fields, concurrent-isolate stress, and silent success/failure checks. No native binary or 107 MB dictionary is committed.

The compiling example/japanese_pyopenjtalk_injected.dart uses a fixed frontend record to demonstrate the public contract without claiming to be pyopenjtalk. The adapter package includes its own real-backend example, offline source build, full resource identities, platform limits, and third-party notices. Linux, Windows, and Intel macOS remain unsupported for this adapter; Cutlet is provided separately as described below.

Japanese Cutlet pipeline

JapaneseCutletEngine ports the pinned Cutlet normalizer and complete 189-entry IPA-oriented kana/symbol mapping, including digraphs, sokuon, moraic-nasal assimilation, long vowels, iteration marks, punctuation spacing, and the upstream tokens == null result contract. It consumes typed JapaneseCutletMorphologyWord records from an explicit JapaneseCutletMorphologyBackend.

The backend receives the exact normalized text and provides raw character types, unknown flags, hiragana readings, and explicit joinWithNext grouping decisions. The compiling example/japanese_cutlet_injected.dart uses one fixed record to demonstrate that public boundary.

The accepted 27-case oracle contains 26 exact outputs and the pinned long-number failure. Pure-Dart replay matches every phoneme string and null token result. Its raw contract captures 126 fugashi records with both nullable UniDic pronunciation and kana fields, selected hiragana, character type, unknown status, and all 12 longest-match joins.

The sibling misakid_mecab_ja package completes Kokoro's default Japanese mode without Python. It distributes the exact licensed MeCab 0.996 source subtree from pyopenjtalk 0.4.1 and compiles a small owned-result ABI through Dart's native-assets hook for Android armv7/arm64/x86-64, iOS device/simulator, and macOS. Callers explicitly supply the validated 20-file UniDic 3.1.0 directory and pinned Misaki ja_words.txt bytes; no dictionary or grouping data is discovered or downloaded. The word list's exact compatibility identity is validated at initialization but it is not redistributed; its undocumented upstream prehistory and inferred Wiktionary/Kaikki lineage are recorded conservatively in THIRD_PARTY_NOTICES.md.

The portable UTF-8/no-iconv build matches all 27 original cases, all 126 raw records and fields, every grouping decision, output, and failure. Tests also cover byte-resource mutation isolation, owned result lifetime, repeated close, input bounds, concurrent isolates, reproducible source identity, exported symbols, resource tampering, and silent native execution. A release Flutter APK contains only the 23 reviewed exports for all three Android ABIs, links exactly libc/libdl/libm, and passes 16 KiB alignment checks. An unsigned iOS device app passes framework, native-assets-manifest, deployment-target, rpath, linkage, and exact-export verification. The complete 27-case fixture passes through the bundled asset on both an Android 15/API 35 arm64 emulator and an iOS 26.4 Simulator. Physical-device and clean hosted-matrix validation remain explicit release gates. Linux, Windows, embedded NUL, and cancellation are unsupported. See packages/misakid_mecab_ja/README.md for the exact resource tuple and build command.

Korean injected pipeline

KoreanG2pkcEngine ports the deterministic idiom, English conversion, morphology annotation, numeral, Jamo, special-rule, table-rule, linking, and rendering stages. It preserves upstream's tokens == null contract and is covered by a 34-case exact matrix: 33 successful outputs plus the pinned 17-digit numeral failure. The typed replay includes ordered CMUdict hit/miss records and exact MeCab morphology streams. Numeral replacement also preserves the observable CPython 3.12 seed-zero set order for overlapping bare numbers, prefixes, and counter spans.

import 'package:misakid/misaki_ko.dart';

G2pResult convertKorean(
  String text,
  KoreanMorphologyBackend morphology,
  KoreanCmuPronunciationProvider cmu,
) {
  return KoreanG2pkcEngine(
    morphology: morphology,
    cmuPronunciations: cmu,
  ).convert(text);
}

The pure package bundles and discovers none of MeCab, its dictionary, NLTK, or CMUdict. The sibling misakid_mecab_ko package supplies an explicit no-Python macOS arm64 adapter. Its offline build verifies official MeCab-ko source and applies a non-terminating safety overlay; callers pass absolute paths to the resulting dylib, the exact 11-file Korean dictionary, and the exact CMUdict data file. CMUdict parsing itself is pure Dart.

The production adapter matches all 34 original morphology streams and 293 surface/tag records, all 23 CMU lookups, all 33 successful final phoneme outputs, and the pinned 17-digit failure. It also covers copied-result ownership, concurrent isolates, lifecycle and bounds, resource tampering, and silent native failure. No native binary or 116 MB of data is committed. Support is precisely macOS 11+ arm64 with the documented resource tuple; Linux, Windows, Intel macOS, embedded NUL, and runtime cancellation remain unsupported.

koreanPhonemeInventory exposes the frozen 67-symbol modern conjoining-Jamo inventory emitted by the default to_syl=false pipeline. Preserved non-Hangul text, punctuation, and whitespace remain outside that phoneme inventory.

Chinese modes

Chinese keeps the pinned legacy IPA renderer and version 1.1 Bopomofo-style frontend as separate contracts. The sibling misakid_chinese package completes legacy/default mode without Python or native code. It validates explicit paths or immutable byte bundles for the exact Jieba 0.42.1 and pypinyin 0.53.0 resources, while cn2an 0.5.23 normalization runs entirely from Dart code:

import 'package:misakid_chinese/misakid_chinese.dart';

Future<G2pResult> convertChinese(String text) async {
  final backend = await PureDartChineseLegacyBackend.open(
    jiebaDictionaryPath: '/absolute/path/jieba/dict.txt',
    jiebaProbabilityStartPath: '/absolute/path/prob_start.p',
    jiebaProbabilityTransitionPath: '/absolute/path/prob_trans.p',
    jiebaProbabilityEmissionPath: '/absolute/path/prob_emit.p',
    pypinyinDictionaryPath: '/absolute/path/pinyin_dict.json',
    pypinyinPhrasesPath: '/absolute/path/phrases_dict.json',
  );
  return ChineseLegacyG2pEngine(backend: backend).convert(text);
}

The six files are not bundled, discovered, or downloaded. Their exact source commits, sizes, checksums, counts, and MIT notices are recorded in packages/misakid_chinese/RESOURCE_MANIFEST.md. After asynchronous validation/loading, conversion is synchronous and offline. All 24 accepted original-Misaki legacy cases pass exactly, including 22 phoneme strings, two pinned failures, 41 Jieba runs, 82 words/pinyin calls, 152 syllables, and null tokens.

Version 1.1 composes typed cn2an, exact Jieba POS/search, the pypinyin large phrase overlay, and the pure ordered tone-sandhi and erhua stages. The sibling package's PureDartChineseFrontend11Backend.open requires the four legacy Jieba files, four POS HMM files, two pypinyin JSON dictionaries, and the exact large_pinyin.txt resource. All eleven resources are explicit and validated; fromResources accepts owned mobile byte bundles when paths are unavailable. No Python or native library is used.

The accepted Chinese-only 26-case matrix matches every final phoneme string and null outer token result, 24 number-normalization calls, 25 POS runs with 131 records, 365 pinyin calls with 746 values, and 107 search calls with 164 segments. The supported no-callback configuration renders each mixed ASCII segment as the selected unknown marker, exactly as pinned upstream. The callback profile is separately accepted through ChineseFrontend11EnglishG2pEngine and the pure-Dart misakid_spacy_en small-model adapter. Its 14-case matrix matches all 14 ordered callback invocations, 40 raw tokenizer records, 33 final English tokens, exact outer phonemes, and null outer token lists across American/British and legacy/2.0 rendering, multiple mixed segments, punctuation, apostrophes/hyphens, context, OOV markers, full-width boundaries, cn2an boundaries, and empty inputs. English preprocessing is fixed on and fallback is fixed off for this precise supported profile. ChineseFrontend11.convertSegment continues to expose the exact internal Chinese tokens for diagnostics.

chinesePhonemeInventory exposes frozen source-derived scalar sets for the distinct contracts: 38 legacy IPA/tone scalars and all 80 frontend-1.1 phone map values. The legacy set excludes punctuation, whitespace, mixed-script pass-through, and backend text. The 1.1 set includes its explicitly mapped tone digits and structural punctuation, space, and slash. Configurable unknown markers and injected English output are outside both inventories.

The compiling example/chinese_injected.dart program shows the injected core contracts; the adapter package has its own complete resource-backed example and security/platform limits.

Vietnamese injected pipeline

VietnameseG2pEngine bundles the reviewed cleaner dictionaries and pure phonology for north, central, and south dialects. It exposes typed tone, glottal, Phạm/Cao, palatal, substring, abbreviation, and acronym options, preserves custom pronunciation controls and onset/nucleus/coda/tone/parent metadata, and accepts an explicit underthesea-compatible tokenizer:

import 'package:misakid/misaki_vi.dart';

G2pResult convertVietnamese(
  String text,
  VietnameseTokenizerBackend tokenizer,
) {
  return VietnameseG2pEngine(
    tokenizer: tokenizer,
    options: const VietnameseOptions(dialect: VietnameseDialect.north),
  ).convert(text);
}

The cleaner runs offline with deterministic generated data and independent clean-room number spelling. Its NFC, whitespace, lowercase/uppercase expansions, supplementary casing, and Final_Sigma decisions do not depend on the host Dart Unicode version. The NFC, case, and whitespace tables are an exhaustively captured CPython-3.11.15/Unicode-14 candidate that still requires the committed exact-3.11.13 comparisons before Vietnamese parity can be claimed. vietnamesePhonemeInventory() returns the frozen 139-entry upstream inventory. The compiling example/vietnamese_injected.dart uses a fixed tokenizer record.

Three 33-case no-English corpora and a strict typed underthesea replay schema are prepared, but no output was fabricated when downloading the exact Python 3.11.13 oracle environment was not authorized. Consequently no authoritative Vietnamese fixture or complete-mode parity is claimed. The package does not bundle underthesea or an English fallback adapter; underthesea 6.8.4 is GPLv3 and its packaged CRF model lacks the precise independent lineage/license record required for a Dart adapter. This remains an experimental injected core.

Hebrew injected facade

HebrewG2pEngine preserves the pinned HEG2P surface: exact text is forwarded with explicit punctuation and stress flags, successful results always have tokens == null, and backend inventory access is defensive and validated.

import 'package:misakid/misaki_he.dart';

G2pResult convertHebrew(String text, HebrewPhonemizerBackend backend) {
  return HebrewG2pEngine(
    backend: backend,
    options: const HebrewOptions(
      preservePunctuation: true,
      preserveStress: true,
    ),
  ).convert(text);
}

The package does not bundle, discover, invoke, or download Mishkal. Callers must provide and validate a compatible backend explicitly, so the facade is an experimental integration surface rather than a supported complete Hebrew mode. Mishkal 0.3.2's exact artifacts are checksum-pinned, but its release metadata declares no license and the later renamed Phonikud project does not establish terms for that historical source/data snapshot. The compiling example/hebrew_injected.dart uses a fixed record to demonstrate the public contract without pretending to be a real Hebrew phonemizer.

Shared G2P contract

Pure in-process engines implement G2pEngine; process- or model-backed adapters implement the separate AsyncG2pEngine contract. Results preserve the important distinction between unavailable token data (null) and an available empty token list:

import 'package:misakid/misaki.dart';

G2pResult convertWith(G2pEngine engine, String text) {
  final result = engine.convert(text);
  // `result.phonemes` is exact: do not trim or normalize it.
  return result;
}

Language tokenizers, morphology analyzers, pinyin providers, native frontends, and model fallbacks are explicit injected backends. The pure library does not download resources, discover executables, invoke Python, or require Flutter.

Development

Normal verification is offline after Dart dependencies have been resolved:

dart pub get
dart format --output=none --set-exit-if-changed .
dart analyze --fatal-infos
dart test
dart pub publish --dry-run

Python fixture generation is a separate, explicit workflow under tool/reference/; parity tests consume committed fixtures rather than regenerating expected results. Some repository data-integrity tests invoke standard-library Python generators in read-only --check mode. Porting decisions and provenance are recorded in PORTING_NOTES.md and THIRD_PARTY_NOTICES.md. The manifest-driven read-only aggregate currently verifies 431 cases across 17 accepted fixture files, with separate interpreters for English transformer, transformer-plus-eSpeak, Cutlet, and the combined Chinese/English callback oracles.

The versioned offline microbenchmark reports cold lexicon loading, warm p50/p95 latency, throughput, resident-memory deltas, and generated runtime-data size:

dart run benchmark/misakid_benchmark.dart --iterations=200

License

Apache License 2.0. Adapted components and data retain their own notices in THIRD_PARTY_NOTICES.md.

Libraries

misaki
Shared, platform-neutral APIs for the Misaki Dart port.
misaki_en
English APIs for the pure, backend-injected Misaki Dart pipeline.
misaki_he
Hebrew-specific APIs for the Misaki Dart port.
misaki_ja
Japanese-specific APIs for the Misaki Dart port.
misaki_ko
Korean APIs for the pure, backend-injected Misaki g2pkc pipeline.
misaki_vi
Vietnamese APIs for the pure, backend-injected Misaki Dart pipeline.
misaki_zh
Chinese-specific APIs for the Misaki Dart port.
misakid
Conventional package-name facade for package:misakid/misaki.dart.