"""RUAccent stresses for the text of the voice. plus_marks(): '+' before the stressed vowel, carried onto the ORIGINAL text word by word. RUAccent is fed lowercase (it is worse on Title-case) and drops spaces before "(", so a char-by-char walk drifts and garbles the tail ("в годод", "))"); here only the n-th Cyrillic word of its output gives its '+' (and ё) to the n-th word of the source, punctuation and spacing stay the source's. If the word counts differ, the text is accented clause by clause. acute_marks(): '+' before a vowel -> the vowel followed by U+0301, the input of the ru-stress model. """ import re VOWELS = "аеёиоуыэюяАЕЁИОУЫЭЮЯ" W = re.compile(r"[А-Яа-яЁё]+(?:-[А-Яа-яЁё]+)*") WP = re.compile(r"[А-Яа-яЁё+]+(?:-[А-Яа-яЁё+]+)*") _acc = None def accentizer(): global _acc if _acc is None: from ruaccent import RUAccent _acc = RUAccent() _acc.load(omograph_model_size="turbo3.1", use_dictionary=True, tiny_mode=False) return _acc def _transfer(orig, acc): out, i = [], 0 for ch in acc: if ch == "+": out.append("+"); continue o = orig[i] out.append(("Ё" if o == "Е" else "ё") if ch.lower() == "ё" and o in "еЕ" else o) i += 1 return "".join(out) def _plus(text): acc = accentizer().process_all(text.lower()) src, dst = list(W.finditer(text)), WP.findall(acc) if len(src) != len(dst) or any(len(a.group(0)) != len(b.replace("+", "")) for a, b in zip(src, dst)): return None out, last = [], 0 for a, b in zip(src, dst): out += [text[last:a.start()], _transfer(a.group(0), b)] last = a.end() return "".join(out) + text[last:] def plus_marks(text: str) -> str: r = _plus(text) if r is not None: return r parts = re.split(r"([,.;:!?()«»…]+\s*)", text) return "".join(p if i % 2 or not p.strip() else (_plus(p) or p) for i, p in enumerate(parts)) def acute_marks(plus_text: str) -> str: return re.sub(r"\+([" + VOWELS + "])", lambda m: m.group(1) + "́", plus_text).replace("+", "")