UNPKG

phonemize

Version:

Fast phonemizer with rule-based G2P prediction. Pure JavaScript implementation.

212 lines (211 loc) 9.49 kB
/** * Per-language Processor abstraction. * * A `LanguageProcessor` bundles the work needed for one language: * text normalization (`preProcess`) and grapheme-to-phoneme prediction * (`predict`). The registry below dispatches by BCP 47 language tag with * script-based fallback. * * The exported `languageRegistry` and free functions wrap a default global * registry to preserve the simple `phonemize("hello")` API. To create * isolated registrations (so multiple language sets can coexist without * stepping on each other) use `new LanguageRegistry()` directly or the * higher-level `createPhonemizer()` factory in `core.ts`. */ /** * A language-bound processor: text normalization plus grapheme-to-phoneme * prediction. Each implementation handles a single language's full * preProcess → predict pipeline; the registry dispatches per-segment. */ export interface LanguageProcessor { /** * Unique identifier for this processor */ readonly id: string; /** * Human-readable name for this processor */ readonly name: string; /** * Languages this processor can handle. Use BCP 47-style tags; the * registry treats `en` as a parent of `en-US`/`en-GB` etc., so a * processor that lists `["en"]` will be selected for `en-GB` requests * unless a more specific processor is also registered. */ readonly supportedLanguages: string[]; /** * Optional language-specific text normalization. Run before tokenization * on a slice of text identified as this processor's language; should * expand numbers, abbreviations, currency, dates, etc. into spoken form. * * Processors without language-specific normalization can leave this * undefined — the tokenizer treats the absence as a no-op. */ preProcess?(text: string): string; /** * Optional per-token POS tag (mostly for homograph disambiguation). * The tokenizer routes each token to the matching processor's * `tagWord` (if any), then passes the returned `pos` into `predict`. * Context is the (optional) immediately neighboring tokens in original * surface form so taggers that need local cues — preceding determiner, * following particle, etc. — have access to them. * * Implementations that don't model POS should leave this undefined; * `predict` will then receive `undefined` for the `pos` argument. */ tagWord?(word: string, context?: { prev?: string; next?: string; }): { pos: string; confidence: number; } | null; /** * Predict phonemes for a given word * * @param word - Word to convert to phonemes * @param language - Language code (optional, for disambiguation) * @param pos - Part of speech (optional, for homograph disambiguation) * @returns Phoneme string in IPA format, or null if cannot process */ predict(word: string, language?: string, pos?: string): string | null; /** * Add a custom pronunciation for a word * * @param word - Word to add pronunciation for * @param pronunciation - IPA pronunciation string */ addPronunciation(word: string, pronunciation: string): void; } /** * Return the primary language subtag — `en-GB` → `en`, `zh-Hant-TW` → `zh`. */ export declare function primaryLang(tag: string): string; /** * Normalize a BCP 47 tag for comparison. The spec defines tag matching * as case-insensitive, so we lowercase before any equality check. */ export declare function normalizeTag(tag: string): string; export declare class LanguageRegistry { private processors; /** Insertion order for stable "first registered wins" semantics. */ private order; register(processor: LanguageProcessor): void; unregister(id: string): boolean; getProcessor(id: string): LanguageProcessor | undefined; getProcessorsForLanguage(language: string): LanguageProcessor[]; getAllProcessors(): LanguageProcessor[]; /** * Find the best processor for a given word and language. When a * language is provided, dialect-exact processors are tried first, * then parent-tag processors. With no language we fall back to the * first registered processor. */ findBestProcessor(_word: string, language?: string): LanguageProcessor | null; predictPhonemes(word: string, language?: string, pos?: string): string | null; clear(): void; getSupportedLanguages(): string[]; } export declare const languageRegistry: LanguageRegistry; /** * Detect the language of the given text based on Unicode character ranges * * @param text - Text to detect language for * @returns Language code or null if not detected */ export declare function detectLanguage(text: string): string | null; /** * Whole-text analysis: identifies the dominant language by character share * and resolves the CJK Han ambiguity (`zh` vs `ja`). * * Han routing heuristic — only **hiragana cluster count** drives the * zh/ja decision: * * `hanIsJa` is true when the text has **two or more** separate * hiragana clusters — i.e. multiple grammatical particles or verb * inflections (は・を・が・で・に・ます・です・ている・…) interleaved * with non-hiragana chars, which is the structural signature of * Japanese prose. * * Why hiragana cluster *count* rather than length or katakana: * * - Taiwan-Chinese text routinely embeds Japanese *loanwords* — * usually katakana (ラーメン, コーヒー, ドラマ) but sometimes * hiragana for food (うどん, おでん, やきとり). These appear as * one isolated kana cluster surrounded by Han, so length-based * rules misfire ("max contiguous hira ≥ 2" would flip `我超愛うどん` * to ja). Counting *separate* clusters skips this class entirely: * a single loanword contributes one cluster. * * - Decorative single hiragana — overwhelmingly `の`, which academic * surveys (Karen S. Chung, *Some Returned Loans*) treat as * functionally identical to 之/的 in Taiwan Mandarin — also contribute * one cluster, so they stay below the threshold. * * - Real Japanese sentences almost always contain two or more * separate hiragana clusters (subject-marker は + verb ending ます, * or any combination of particles). Even short ones like 田中さんは * trip the rule via `さん` + `は`. * * Trade-offs: * - Single-particle Japanese fragments (`今日は`, `頑張って`) and * single-word Japanese (`私の本`) won't trip the flag — accepted * as out-of-context ambiguity that needs `options.language: "ja"` * to disambiguate. * - Pure-katakana brand names (`スターバックス`) and kana-only * words (`ねこ`, `こんにちは`) don't need hanIsJa anyway: they * carry no Han to mis-dispatch, and the character-share count * below still puts `ja` on top so neutral runs route correctly. * * Primary language is the bucket with the most characters after han→ja * reassignment, or undefined when the text has no non-neutral chars at all. */ export interface TextAnalysis { /** Dominant language by character share; undefined for pure-neutral text. */ primary?: string; /** When true, Han chars in this text should be routed as Japanese. */ hanIsJa: boolean; } export declare function analyzeText(text: string): TextAnalysis; export interface ScriptRun { text: string; /** Detected language code; empty string when the run contains only neutrals. */ lang: string; } /** * Split text into script-based runs. Neutral characters (digits, whitespace, * punctuation) attach to the surrounding non-neutral run instead of starting * a new one, so e.g. `"我有 3 本书"` stays one Chinese run rather than * fragmenting into `[zh, en-digit, zh]`. * * Pass `hanIsJa: true` (typically from a prior `analyzeText` call) to route * Han chars through the Japanese path instead of the default Chinese path — * essential for Japanese prose, which mixes kanji with kana. */ export declare function splitByScript(text: string, options?: { hanIsJa?: boolean; }): ScriptRun[]; /** * Run each script-run through its language processor's `preProcess`. * * Resolution order for each run's effective language: * 1. The run's own detected script (zh/ja/ko/…) * 2. The caller-supplied `defaultLang` (e.g. `options.language`) * 3. The document's primary language inferred by `analyzeText` * * Step 3 ensures that pure-neutral inputs like `"123"` get the right * expansion when the surrounding context (or another part of the same * call) is non-English. */ export declare function preProcessByScript(text: string, registry: LanguageRegistry, defaultLang?: string): string; /** * Register a language processor on the default global registry. * * For multi-instance setups, prefer `createPhonemizer()` from `./core`. */ export declare function useProcessor(processor: LanguageProcessor): void; export declare function findProcessor(word: string, language?: string): LanguageProcessor | null; export declare function predictPhonemes(word: string, language?: string, pos?: string): string | null; export declare function getRegisteredProcessorIds(): string[]; export declare function getProcessorsForLanguage(language: string): LanguageProcessor[]; export declare function getSupportedLanguages(): string[];