Spaces:
Runtime error
Runtime error
Download dizione/engine.py from hugging-apps/tiny-voca: direct link, hf CLI and curl.
- Browser
- Download file 12.8 kB
-
https://huggingface.co/spaces/hugging-apps/tiny-voca/resolve/main/dizione/engine.py
- Command line
-
hf download hf://spaces/hugging-apps/tiny-voca/dizione/engine.py
-
curl -L -o engine.py https://huggingface.co/spaces/hugging-apps/tiny-voca/resolve/main/dizione/engine.py
12.8 kB
| """The phonemizer: text -> spoken words of phones, clause by clause. | |
| Order of authority for one word: | |
| 1. pronunciation written inline in the text | |
| 2. the user's override list | |
| 3. the built-in exception list (``data/exceptions.txt``); a name of several | |
| words listed there or by the user is spoken with the stress written | |
| 4. function words and function-word groups (sentence level) | |
| 5. the dictionary built from CMUdict | |
| 6. regular endings, prefixes and compounds of dictionary words | |
| 7. the unknown-word model | |
| """ | |
| import json | |
| import os | |
| from pathlib import Path | |
| from . import morphology, normalize, sentence, tables | |
| from .align import Aligner | |
| from .convert import convert, from_text, settle_stress | |
| from .g2p import Model | |
| from .homographs import Homographs | |
| from .sentence import Unit | |
| ENVIRONMENT = "DIZIONE_DATA" | |
| OVERRIDES_ENVIRONMENT = "DIZIONE_OVERRIDES" | |
| _LETTER_NAMES = None | |
| def default_directory(): | |
| """Where the built files live: ``$DIZIONE_DATA`` or ``built`` inside the package.""" | |
| return Path(os.environ.get(ENVIRONMENT) or Path(__file__).parent / "built") | |
| class Phonemizer: | |
| def __init__(self, directory=None, overrides=None, join_phrases=True): | |
| self.directory = Path(directory) if directory else default_directory() | |
| lexicon = self.directory / "lexicon.tsv" | |
| if not lexicon.exists(): | |
| raise FileNotFoundError( | |
| f"no phonemizer data in {self.directory}: build it with scripts/build.sh" | |
| f"or point ${ENVIRONMENT} at a built directory") | |
| self.lexicon = dict(line.split("\t", 1) for line in lexicon.read_text(encoding="utf-8").splitlines() if line) | |
| self.aligner = Aligner(json.loads((self.directory / "align.json").read_text(encoding="utf-8"))) | |
| self.model = Model(self.directory) if (self.directory / "g2p.npz").exists() else None | |
| self.homographs = Homographs(self.directory / "homographs.json") if (self.directory / "homographs.json").exists() else None | |
| self.functions = tables.function_words() | |
| self.phrases = tables.phrases() if join_phrases else {} | |
| self.phrase_length = max((len(key) for key in self.phrases), default=1) | |
| self.exceptions = tables.pronunciations("exceptions.txt") | |
| self.names = tables.names("exceptions.txt") # names of several words, with their own rhythm | |
| self.name_starts = {key[0] for key in self.names} | |
| self.name_length = max((len(key) for key in self.names), default=1) | |
| self.user_names = set() | |
| self.overrides = {} | |
| override_file = os.environ.get(OVERRIDES_ENVIRONMENT) | |
| if override_file: | |
| self.add_overrides(Path(override_file)) | |
| if overrides: | |
| self.add_overrides(overrides) | |
| self.normalizer = normalize.Normalizer(self.is_word) | |
| self._cache = {} | |
| # ---- single words ------------------------------------------------- | |
| def add_overrides(self, source): | |
| """Pronunciations that win over everything else. ``source`` is a | |
| dictionary ``{word: pronunciation}``, a path, or the text of a table; | |
| a pronunciation is CMUdict notation or phones with stress marks. A | |
| name of several words takes one pronunciation per word, separated by | |
| ``|``: ``{"tung tung tung sahur": "T UW1 NG | T UW2 NG | T UW2 NG | S AA0 HH UH1 R"}``.""" | |
| self.overrides.update(tables.pronunciations(source)) | |
| taught = tables.names(source) | |
| self.names.update(taught) | |
| self.user_names.update(taught) | |
| self.name_starts = {key[0] for key in self.names} | |
| self.name_length = max((len(key) for key in self.names), default=1) | |
| self._cache = {} | |
| def _entry(self, word, entry): | |
| kind, value = entry | |
| if kind == "phones": | |
| return tuple(value) | |
| return convert(word, value, self.aligner.align(word, value)) | |
| def known(self, word): | |
| """Pronunciation from the override list, the exceptions or the dictionary.""" | |
| if word in self.overrides: | |
| return self._entry(word, self.overrides[word]) | |
| if word in self.exceptions: | |
| return self._entry(word, self.exceptions[word]) | |
| text = self.lexicon.get(word) | |
| return from_text(text) if text is not None else None | |
| def ordinary(self, word): | |
| """Pronunciation of a dictionary word that may be half of a compound. | |
| CMUdict is full of surnames; a word that is also listed with a regular | |
| ending ("webs", "screens") is an ordinary one.""" | |
| if len(word) < 3 or not any(word + ending in self.lexicon for ending in ("s", "es", "ed", "ing")): | |
| return None | |
| return self.known(word) | |
| def guess(self, word): | |
| """The unknown-word model, then the conversion rules, on one spelling.""" | |
| if self.model is None: | |
| return self.spell(word) | |
| spelling = self.model.spellable(word) | |
| symbols = self.model.predict(spelling) | |
| # The model emits each phoneme on its own, and between two possible vowels before | |
| # an r it sometimes emits neither ("okafor" -> ... F R). An r with no vowel beside | |
| # it is a syllable: write it as one. | |
| for k, symbol in enumerate(symbols): | |
| if symbol == "R" and not any(0 <= j < len(symbols) and symbols[j][-1] in "012" for j in (k - 1, k + 1)): | |
| symbols[k] = "ER0" | |
| if not any(symbol[-1] in "012" for symbol in symbols): | |
| return self.spell(word) | |
| # Every word spoken alone has a main stress, even when the model marked none. | |
| return sentence.promote(convert(spelling, symbols, self.aligner.align(spelling, symbols))) | |
| def spell(self, letters): | |
| """Letter names, joined: the last letter carries the main stress.""" | |
| global _LETTER_NAMES | |
| if _LETTER_NAMES is None: | |
| _LETTER_NAMES = {key: from_text(value) for key, value in tables.mapping("letters.txt").items()} | |
| possessive = letters.lower().endswith("'s") | |
| names = [_LETTER_NAMES[c] for c in (letters[:-2] if possessive else letters).lower() if c in _LETTER_NAMES] | |
| out = [] | |
| for index, name in enumerate(names): | |
| out.extend((phone, 2 if stress and index < len(names) - 1 else stress) for phone, stress in name) | |
| return morphology.plural(tuple(out)) if possessive and out else tuple(out) | |
| def is_word(self, word): | |
| """True for a spelling the tables or the dictionary know.""" | |
| return word in self.lexicon or word in self.functions or word in self.exceptions or word in self.overrides | |
| def word(self, word): | |
| """``(phones, source)`` for one spelling in lower case.""" | |
| if word in self._cache: | |
| return self._cache[word] | |
| phones, source = self.known(word), "dictionary" | |
| if phones is None: | |
| phones, source = morphology.derive(word, self.known, self.ordinary), "ending" | |
| if phones is not None: | |
| phones = settle_stress(phones) | |
| if phones is None and word.endswith("'s") and len(word) > 3: | |
| phones, source = morphology.plural(self.guess(word[:-2])), "model" | |
| if phones is None: | |
| phones, source = self.guess(word), "model" | |
| self._cache[word] = (phones, source) | |
| return phones, source | |
| # ---- sentences ---------------------------------------------------- | |
| def _units(self, words, places=None, written=None): | |
| """Units of one clause from its ``normalize.Word`` tokens. ``written`` | |
| is the whole text as tokens and ``places`` where each word sits in it, | |
| for the homograph rules.""" | |
| units, index = [], 0 | |
| while index < len(words): | |
| token = words[index] | |
| if token.phones is not None: | |
| units.append(Unit(token.text, tuple(token.phones), "p", attach=token.attach, source="inline")) | |
| elif token.spell: | |
| units.append(Unit(token.text, self.spell(token.text), "p", attach=token.attach, source="letters")) | |
| else: | |
| size = self._name(words, index) if token.text in self.name_starts else 0 | |
| if size: # a taught name: every word as written in the table | |
| key = tuple(w.text for w in words[index:index + size]) | |
| units.extend(Unit(text, self._entry(text, entry), "p", attach=token.attach and position == 0, source="name") | |
| for position, (text, entry) in enumerate(zip(key, self.names[key]))) | |
| index += size | |
| continue | |
| size = self._phrase(words, index) | |
| if size: | |
| key = tuple(w.text for w in words[index:index + size]) | |
| kind, forms, final = self.phrases[key] | |
| if len(forms) == 1: | |
| units.append(Unit(" ".join(key), forms[0], kind, final, token.attach, "phrase")) | |
| else: # the boundaries stay, only the stress is fixed | |
| units.extend(Unit(text, form, kind, attach=token.attach and position == 0, source="phrase") | |
| for position, (text, form) in enumerate(zip(key, forms))) | |
| index += size | |
| continue | |
| if token.text in self.overrides: | |
| units.append(Unit(token.text, self.known(token.text), "c", attach=token.attach, source="override")) | |
| elif self.homographs and written and self.homographs.covers(token.text) and token.text not in self.exceptions: | |
| phones, ending = self.homographs.pronounce(token.text, written, places[index]) | |
| phones = morphology.plural(from_text(phones)) if ending else from_text(phones) | |
| units.append(Unit(token.text, phones, "c", attach=token.attach, source="homograph")) | |
| elif token.text in self.functions: | |
| kind, form, final = self.functions[token.text] | |
| units.append(Unit(token.text, form, kind, final, token.attach, "function")) | |
| else: | |
| phones, source = self.word(token.text) | |
| units.append(Unit(token.text, phones, "c", attach=token.attach, source=source)) | |
| index += 1 | |
| return units | |
| def _name(self, words, index): | |
| for size in range(min(self.name_length, len(words) - index), 1, -1): | |
| span = words[index:index + size] | |
| key = tuple(w.text for w in span) | |
| if key in self.names and all(w.phones is None and not w.spell for w in span) and not any(w.attach for w in span[1:]) \ | |
| and (key in self.user_names or not any(w.text in self.overrides for w in span)): | |
| return size # the user's own word list wins over a built-in name | |
| return 0 | |
| def _phrase(self, words, index): | |
| for size in range(min(self.phrase_length, len(words) - index), 1, -1): | |
| span = words[index:index + size] | |
| if all(w.phones is None and not w.spell and not w.attach for w in span[1:]) and span[0].phones is None \ | |
| and not span[0].spell and tuple(w.text for w in span) in self.phrases \ | |
| and not any(w.text in self.overrides for w in span): | |
| return size | |
| return 0 | |
| def analyse(self, text): | |
| """Clauses and marks in order: ``("punct", mark)`` or ``("clause", [Unit, ...])``.""" | |
| stream = self.normalizer.tokens(text) | |
| # The text as the homograph rules see it; a word given only as phones stands as "x". | |
| written = [token.mark if isinstance(token, normalize.Punct) else (token.raw or token.text or "x") for token in stream] | |
| out, words, places = [], [], [] | |
| def flush(): | |
| if words: | |
| units = self._units(words, places, written) | |
| sentence.stress_clause(units) | |
| sentence.join_clause(units) | |
| out.append(("clause", units)) | |
| words.clear() | |
| places.clear() | |
| for place, token in enumerate(stream): | |
| if isinstance(token, normalize.Punct): | |
| flush() | |
| out.append(("punct", token.mark)) | |
| else: | |
| words.append(token) | |
| places.append(place) | |
| flush() | |
| return out | |
| def pieces(self, text): | |
| """The structure ``symbols.tokenize`` takes: ``(True, mark)`` for | |
| punctuation, ``(False, [[(phone, stress), ...], ...])`` for a run of words.""" | |
| return [(True, value) if kind == "punct" else (False, sentence.groups(value)) for kind, value in self.analyse(text)] | |